Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/client/src/components/app_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ export type CommandMappings = {
exportAsPdf: CommandData;
openNoteExternally: CommandData;
openNoteCustom: CommandData;
openNoteOnServer: CommandData;
renderActiveNote: CommandData;
unhoist: CommandData;
reloadFrontendApp: CommandData;
Expand Down
7 changes: 7 additions & 0 deletions apps/client/src/components/root_command_executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ export default class RootCommandExecutor extends Component {
}
}

openNoteOnServerCommand() {
const noteId = appContext.tabManager.getActiveContextNoteId();
if (noteId) {
openService.openNoteOnServer(noteId);
}
}

enterProtectedSessionCommand() {
protectedSessionService.enterProtectedSession();
}
Expand Down
17 changes: 17 additions & 0 deletions apps/client/src/services/open.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import utils from "./utils.js";
import server from "./server.js";
import options from "./options.js";

type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void;

Expand Down Expand Up @@ -171,6 +172,21 @@ function getHost() {
return `${url.protocol}//${url.hostname}:${url.port}`;
}

async function openNoteOnServer(noteId: string) {
// Get the sync server host from options
const syncServerHost = options.get("syncServerHost");

if (!syncServerHost) {
console.error("No sync server host configured");
return;
}

const url = new URL(`#root/${noteId}`, syncServerHost).toString();

// Use window.open to ensure link opens in external browser in Electron
window.open(url, '_blank', 'noopener,noreferrer');
}

async function openDirectory(directory: string) {
try {
if (utils.isElectron()) {
Expand Down Expand Up @@ -198,5 +214,6 @@ export default {
openAttachmentExternally,
openNoteCustom,
openAttachmentCustom,
openNoteOnServer,
openDirectory
};
1 change: 1 addition & 0 deletions apps/client/src/translations/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@
"open_note_externally": "Open note externally",
"open_note_externally_title": "File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.",
"open_note_custom": "Open note custom",
"open_note_on_server": "Open note on server",
"import_files": "Import files",
"export_note": "Export note",
"delete_note": "Delete note",
Expand Down
7 changes: 6 additions & 1 deletion apps/client/src/widgets/ribbon/NoteActions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
import { ConvertToAttachmentResponse, OptionNames } from "@triliumnext/commons";
import appContext, { CommandNames } from "../../components/app_context";
import FNote from "../../entities/fnote"
import dialog from "../../services/dialog";
Expand All @@ -12,6 +12,7 @@ import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
import { ParentComponent } from "../react/react_utils";
import { useContext } from "preact/hooks";
import { useTriliumOption } from "../react/hooks";
import NoteContext from "../../components/note_context";
import branches from "../../services/branches";

Expand Down Expand Up @@ -52,6 +53,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type);
const isSearchOrBook = ["search", "book"].includes(note.type);
const [ syncServerHost ] = useTriliumOption("syncServerHost");

return (
<Dropdown
Expand Down Expand Up @@ -82,6 +84,9 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
<CommandItem command="openNoteExternally" icon="bx bx-file-find" disabled={isSearchOrBook || !isElectron} text={t("note_actions.open_note_externally")} title={t("note_actions.open_note_externally_title")} />
<CommandItem command="openNoteCustom" icon="bx bx-customize" disabled={isSearchOrBook || isMac || !isElectron} text={t("note_actions.open_note_custom")} />
<CommandItem command="showNoteSource" icon="bx bx-code" disabled={!hasSource} text={t("note_actions.note_source")} />
{(syncServerHost && isElectron) &&
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The disabled={!syncServerHost} prop is redundant here. The CommandItem is only rendered if syncServerHost is truthy due to the conditional rendering on line 87. In that case, !syncServerHost will always evaluate to false. You can remove this prop for cleaner and more maintainable code.

Suggested change
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
<CommandItem command="openNoteOnServer" icon="bx bx-world" text={t("note_actions.open_note_on_server")} />

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist it is redundant, but I would still keep disabled attribute to be replaced with potential another check - for notes that cannot be opened on server. I haven't identified if there are such notes - notes like Options and User Guide are still openable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your intention to keep the disabled attribute for potential future checks. However, the current disabled={!syncServerHost} is still redundant.

Here's why:

  • The CommandItem on line 88 is only rendered if syncServerHost is truthy (due to (syncServerHost && isElectron) on line 87).
  • If syncServerHost is truthy, then !syncServerHost will always evaluate to false.
  • Therefore, the disabled prop will effectively always be disabled={false} when this CommandItem is visible.

If you plan to add more complex disabling logic in the future (e.g., based on the note type), you would modify this disabled prop anyway. Removing the currently redundant disabled={!syncServerHost} now would simplify the code without hindering future enhancements, as you'd be adding the new logic to a clean slate. The current disabled={!syncServerHost} doesn't serve as a useful placeholder for future, more complex disabling conditions.

Suggested change
<CommandItem command="openNoteOnServer" icon="bx bx-world" disabled={!syncServerHost} text={t("note_actions.open_note_on_server")} />
<CommandItem command="openNoteOnServer" icon="bx bx-world" text={t("note_actions.open_note_on_server")} />

Copy link

@werererer werererer Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@contributor I'd go here with gemini, had the same argument in my head. I can't imagine why the server shouldn't support a note type that is supported on desktop that would be instantly a Bug.

Copy link
Contributor Author

@contributor contributor Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, folder view - a note that shows content of a local folder. You are likely won't have such note on server due to security reason and file system path differences.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@contributor I think even syncing up a potential folder view would be beneficial and it then would just open with error, it is only configured for X, Y, Z. Because the structure reminds the user oh wait I might have to open it up on my Computer.

Copy link

@werererer werererer Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@contributor however I might see an edge case where you'd be right. Assume you upload images that you dont want to be shared on your trilium server instance. Then not having them synced up, maybe if there would be an option to hide this from the server, that would be beneficial, making the approach ofc worth keep having.

I would think this would resolve the comments then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, local only (not synced) notes too. Evernote had it 15+ years ago. And if you don't trust the trilium sync server - that could be useful feature (even more true if they turn trilium to multi-user server)

}
<FormDropdownDivider />

<CommandItem command="forceSaveRevision" icon="bx bx-save" disabled={isInOptionsOrHelp} text={t("note_actions.save_revision")} />
Expand Down