From b06cdd442d36f790088916de807dc4b2d72bdf57 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 12 Mar 2026 20:41:53 +0200 Subject: [PATCH 1/8] fix(calendar): does not respect protected note of parent --- apps/client/src/services/note_create.ts | 30 +++++++++++-------- .../src/widgets/collections/calendar/api.ts | 10 ++++--- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/apps/client/src/services/note_create.ts b/apps/client/src/services/note_create.ts index 00ae717d21..c4441e8692 100644 --- a/apps/client/src/services/note_create.ts +++ b/apps/client/src/services/note_create.ts @@ -1,15 +1,17 @@ +import type { CKTextEditor } from "@triliumnext/ckeditor5"; +import { AttributeRow } from "@triliumnext/commons"; + import appContext from "../components/app_context.js"; +import type FBranch from "../entities/fbranch.js"; +import type FNote from "../entities/fnote.js"; +import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; +import froca from "./froca.js"; +import { t } from "./i18n.js"; import protectedSessionHolder from "./protected_session_holder.js"; import server from "./server.js"; -import ws from "./ws.js"; -import froca from "./froca.js"; -import treeService from "./tree.js"; import toastService from "./toast.js"; -import { t } from "./i18n.js"; -import type FNote from "../entities/fnote.js"; -import type FBranch from "../entities/fbranch.js"; -import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; -import type { CKTextEditor } from "@triliumnext/ckeditor5"; +import treeService from "./tree.js"; +import ws from "./ws.js"; export interface CreateNoteOpts { isProtected?: boolean; @@ -24,6 +26,8 @@ export interface CreateNoteOpts { target?: string; targetBranchId?: string; textEditor?: CKTextEditor; + /** Attributes to be set on the note. These are set atomically on note creation, so entity changes are not sent for attributes defined here. */ + attributes?: Omit[]; } interface Response { @@ -37,7 +41,7 @@ interface DuplicateResponse { note: FNote; } -async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}) { +async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}, componentId?: string) { options = Object.assign( { activate: true, @@ -77,8 +81,9 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot isProtected: options.isProtected, type: options.type, mime: options.mime, - templateNoteId: options.templateNoteId - }); + templateNoteId: options.templateNoteId, + attributes: options.attributes + }, componentId); if (options.saveSelection) { // we remove the selection only after it was saved to server to make sure we don't lose anything @@ -140,9 +145,8 @@ function parseSelectedHtml(selectedHtml: string) { const content = selectedHtml.replace(dom[0].outerHTML, ""); return [title, content]; - } else { - return [null, selectedHtml]; } + return [null, selectedHtml]; } async function duplicateSubtree(noteId: string, parentNotePath: string) { diff --git a/apps/client/src/widgets/collections/calendar/api.ts b/apps/client/src/widgets/collections/calendar/api.ts index 79df65e4c1..b74f5047b6 100644 --- a/apps/client/src/widgets/collections/calendar/api.ts +++ b/apps/client/src/widgets/collections/calendar/api.ts @@ -1,8 +1,8 @@ -import { AttributeRow, CreateChildrenResponse } from "@triliumnext/commons"; +import { AttributeRow } from "@triliumnext/commons"; import FNote from "../../../entities/fnote"; import { setAttribute, setLabel } from "../../../services/attributes"; -import server from "../../../services/server"; +import note_create from "../../../services/note_create"; interface NewEventOpts { title: string; @@ -51,11 +51,13 @@ export async function newEvent(parentNote: FNote, { title, startDate, endDate, s } // Create the note. - await server.post(`notes/${parentNote.noteId}/children?target=into`, { + await note_create.createNote(parentNote.noteId, { title, + isProtected: parentNote.isProtected, content: "", type: "text", - attributes + attributes, + activate: false }, componentId); } From a6a1594265f627dd2f1f6cb8bc0998aa5228f771 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 12 Mar 2026 20:44:25 +0200 Subject: [PATCH 2/8] fix(table): does not respect protected note of parent --- .../src/widgets/collections/table/index.tsx | 4 +-- .../widgets/collections/table/row_editing.ts | 33 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/apps/client/src/widgets/collections/table/index.tsx b/apps/client/src/widgets/collections/table/index.tsx index 87745b3e2d..b4c49faf70 100644 --- a/apps/client/src/widgets/collections/table/index.tsx +++ b/apps/client/src/widgets/collections/table/index.tsx @@ -18,14 +18,14 @@ import useRowTableEditing from "./row_editing"; import { TableData } from "./rows"; import Tabulator from "./tabulator"; -export default function TableView({ note, noteIds, notePath, viewConfig, saveConfig }: ViewModeProps) { +export default function TableView({ note, noteIds, viewConfig, saveConfig }: ViewModeProps) { const tabulatorRef = useRef(null); const parentComponent = useContext(ParentComponent); const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget().contentSized()); const contextMenuEvents = useContextMenu(note, parentComponent, tabulatorRef); const persistenceProps = usePersistence(viewConfig, saveConfig); - const rowEditingEvents = useRowTableEditing(tabulatorRef, attributeDetailWidget, notePath); + const rowEditingEvents = useRowTableEditing(tabulatorRef, attributeDetailWidget, note); const { newAttributePosition, resetNewAttributePosition } = useColTableEditing(tabulatorRef, attributeDetailWidget, note); const { columnDefs, rowData, movableRows, hasChildren } = useData(note, noteIds, viewConfig, newAttributePosition, resetNewAttributePosition); const dataTreeProps = useMemo(() => { diff --git a/apps/client/src/widgets/collections/table/row_editing.ts b/apps/client/src/widgets/collections/table/row_editing.ts index 22ef0e7e48..c4df69e7ae 100644 --- a/apps/client/src/widgets/collections/table/row_editing.ts +++ b/apps/client/src/widgets/collections/table/row_editing.ts @@ -1,24 +1,27 @@ -import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables"; -import { CommandListenerData } from "../../../components/app_context"; -import note_create, { CreateNoteOpts } from "../../../services/note_create"; -import { useLegacyImperativeHandlers } from "../../react/hooks"; import { RefObject } from "preact"; -import { setAttribute, setLabel } from "../../../services/attributes"; -import froca from "../../../services/froca"; -import server from "../../../services/server"; -import branches from "../../../services/branches"; -import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; +import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables"; -export default function useRowTableEditing(api: RefObject, attributeDetailWidget: AttributeDetailWidget, parentNotePath: string): Partial { +import { CommandListenerData } from "../../../components/app_context"; +import FNote from "../../../entities/fnote"; +import { setAttribute, setLabel } from "../../../services/attributes"; +import branches from "../../../services/branches"; +import froca from "../../../services/froca"; +import note_create, { CreateNoteOpts } from "../../../services/note_create"; +import server from "../../../services/server"; +import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; +import { useLegacyImperativeHandlers } from "../../react/hooks"; + +export default function useRowTableEditing(api: RefObject, attributeDetailWidget: AttributeDetailWidget, parentNote: FNote): Partial { // Adding new rows useLegacyImperativeHandlers({ addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { - const notePath = customNotePath ?? parentNotePath; + const notePath = customNotePath ?? parentNote.noteId; if (notePath) { const opts: CreateNoteOpts = { activate: false, + isProtected: parentNote.isProtected, ...customOpts - } + }; note_create.createNote(notePath, opts).then(({ branch }) => { if (branch) { setTimeout(() => { @@ -26,7 +29,7 @@ export default function useRowTableEditing(api: RefObject, attributeD focusOnBranch(api.current, branch?.branchId); }, 100); } - }) + }); } } }); @@ -91,14 +94,14 @@ function focusOnBranch(api: Tabulator, branchId: string) { } function findRowDataById(rows: RowComponent[], branchId: string): RowComponent | null { - for (let row of rows) { + for (const row of rows) { const item = row.getIndex() as string; if (item === branchId) { return row; } - let found = findRowDataById(row.getTreeChildren(), branchId); + const found = findRowDataById(row.getTreeChildren(), branchId); if (found) return found; } return null; From 4ab3b0dd2bc3c010eaa2aa222f940debbc074a0b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 12 Mar 2026 20:47:43 +0200 Subject: [PATCH 3/8] fix(map): does not respect protected note of parent --- .../src/widgets/collections/geomap/api.ts | 17 +++++++++++------ .../widgets/collections/geomap/context_menu.ts | 14 ++++++++------ .../src/widgets/collections/geomap/index.tsx | 8 ++++---- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/client/src/widgets/collections/geomap/api.ts b/apps/client/src/widgets/collections/geomap/api.ts index 5f73415605..4a839cac5d 100644 --- a/apps/client/src/widgets/collections/geomap/api.ts +++ b/apps/client/src/widgets/collections/geomap/api.ts @@ -1,10 +1,11 @@ import type { LatLng, LeafletMouseEvent } from "leaflet"; -import { LOCATION_ATTRIBUTE } from "."; + +import FNote from "../../../entities/fnote"; import attributes from "../../../services/attributes"; import { prompt } from "../../../services/dialog"; -import server from "../../../services/server"; import { t } from "../../../services/i18n"; -import { CreateChildrenResponse } from "@triliumnext/commons"; +import note_create from "../../../services/note_create"; +import { LOCATION_ATTRIBUTE } from "."; const CHILD_NOTE_ICON = "bx bx-pin"; @@ -13,15 +14,19 @@ export async function moveMarker(noteId: string, latLng: LatLng | null) { await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value); } -export async function createNewNote(noteId: string, e: LeafletMouseEvent) { +export async function createNewNote(parentNote: FNote, e: LeafletMouseEvent) { const title = await prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") }); if (title?.trim()) { - const { note } = await server.post(`notes/${noteId}/children?target=into`, { + const { note } = await note_create.createNote(parentNote.noteId, { title, content: "", - type: "text" + type: "text", + activate: false, + isProtected: parentNote.isProtected }); + if (!note) return; + attributes.setLabel(note.noteId, "iconClass", CHILD_NOTE_ICON); moveMarker(note.noteId, e.latlng); } diff --git a/apps/client/src/widgets/collections/geomap/context_menu.ts b/apps/client/src/widgets/collections/geomap/context_menu.ts index 47026566fc..b4ae723877 100644 --- a/apps/client/src/widgets/collections/geomap/context_menu.ts +++ b/apps/client/src/widgets/collections/geomap/context_menu.ts @@ -1,12 +1,14 @@ import type { LatLng, LeafletMouseEvent } from "leaflet"; + import appContext, { type CommandMappings } from "../../../components/app_context.js"; +import FNote from "../../../entities/fnote.js"; import contextMenu, { type MenuItem } from "../../../menus/context_menu.js"; -import linkContextMenu from "../../../menus/link_context_menu.js"; import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker.jsx"; -import { t } from "../../../services/i18n.js"; -import { createNewNote } from "./api.js"; +import linkContextMenu from "../../../menus/link_context_menu.js"; import { copyTextWithToast } from "../../../services/clipboard_ext.js"; +import { t } from "../../../services/i18n.js"; import link from "../../../services/link.js"; +import { createNewNote } from "./api.js"; export default function openContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) { let items: MenuItem[] = [ @@ -44,7 +46,7 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is }); } -export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) { +export function openMapContextMenu(note: FNote, e: LeafletMouseEvent, isEditable: boolean) { let items: MenuItem[] = [ ...buildGeoLocationItem(e) ]; @@ -55,10 +57,10 @@ export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEdita { kind: "separator" }, { title: t("geo-map-context.add-note"), - handler: () => createNewNote(noteId, e), + handler: () => createNewNote(note, e), uiIcon: "bx bx-plus" } - ] + ]; } contextMenu.show({ diff --git a/apps/client/src/widgets/collections/geomap/index.tsx b/apps/client/src/widgets/collections/geomap/index.tsx index 8be547fa3a..cb4b33b5af 100644 --- a/apps/client/src/widgets/collections/geomap/index.tsx +++ b/apps/client/src/widgets/collections/geomap/index.tsx @@ -93,14 +93,14 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM const onClick = useCallback(async (e: LeafletMouseEvent) => { if (state === State.NewNote) { toast.closePersistent("geo-new-note"); - await createNewNote(note.noteId, e); + await createNewNote(note, e); setState(State.Normal); } - }, [ state ]); + }, [ note, state ]); const onContextMenu = useCallback((e: LeafletMouseEvent) => { - openMapContextMenu(note.noteId, e, !isReadOnly); - }, [ note.noteId, isReadOnly ]); + openMapContextMenu(note, e, !isReadOnly); + }, [ note, isReadOnly ]); // Dragging const containerRef = useRef(null); From 5abb77242c9a667805d0b7c783ce36a14c582f84 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 12 Mar 2026 20:49:34 +0200 Subject: [PATCH 4/8] feat(map): create pins atomically --- apps/client/src/widgets/collections/geomap/api.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/client/src/widgets/collections/geomap/api.ts b/apps/client/src/widgets/collections/geomap/api.ts index 4a839cac5d..76c05e637f 100644 --- a/apps/client/src/widgets/collections/geomap/api.ts +++ b/apps/client/src/widgets/collections/geomap/api.ts @@ -18,16 +18,16 @@ export async function createNewNote(parentNote: FNote, e: LeafletMouseEvent) { const title = await prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") }); if (title?.trim()) { - const { note } = await note_create.createNote(parentNote.noteId, { + await note_create.createNote(parentNote.noteId, { title, content: "", type: "text", activate: false, - isProtected: parentNote.isProtected + isProtected: parentNote.isProtected, + attributes: [ + { type: "label", name: LOCATION_ATTRIBUTE, value: [e.latlng.lat, e.latlng.lng].join(",") }, + { type: "label", name: "iconClass", value: CHILD_NOTE_ICON } + ] }); - if (!note) return; - - attributes.setLabel(note.noteId, "iconClass", CHILD_NOTE_ICON); - moveMarker(note.noteId, e.latlng); } } From 744b93dd980026c4370baf089a50add9783ca256 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 12 Mar 2026 20:50:56 +0200 Subject: [PATCH 5/8] fix(board): does not respect protected note of parent --- .../src/widgets/collections/board/api.ts | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/client/src/widgets/collections/board/api.ts b/apps/client/src/widgets/collections/board/api.ts index af88f935e5..16237edd52 100644 --- a/apps/client/src/widgets/collections/board/api.ts +++ b/apps/client/src/widgets/collections/board/api.ts @@ -1,5 +1,5 @@ import { BulkAction } from "@triliumnext/commons"; -import { BoardViewData } from "."; + import appContext from "../../../components/app_context"; import FNote from "../../../entities/fnote"; import attributes from "../../../services/attributes"; @@ -9,6 +9,7 @@ import froca from "../../../services/froca"; import { t } from "../../../services/i18n"; import note_create from "../../../services/note_create"; import server from "../../../services/server"; +import { BoardViewData } from "."; import { ColumnMap } from "./data"; export default class BoardApi { @@ -35,13 +36,11 @@ export default class BoardApi { async createNewItem(column: string, title: string) { try { - // Get the parent note path - const parentNotePath = this.parentNote.noteId; - // 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(this.parentNote.noteId, { activate: false, - title + title, + isProtected: this.parentNote.isProtected }); if (newNote && newBranch) { @@ -87,7 +86,7 @@ export default class BoardApi { const action: BulkAction = this.isRelationMode ? { name: "deleteRelation", relationName: this.statusAttribute } - : { name: "deleteLabel", labelName: this.statusAttribute } + : { name: "deleteLabel", labelName: this.statusAttribute }; await executeBulkActions(noteIds, [ action ]); this.viewConfig.columns = (this.viewConfig.columns ?? []).filter(col => col.value !== column); this.saveConfig(this.viewConfig); @@ -99,7 +98,7 @@ export default class BoardApi { // Change the value in the notes. const action: BulkAction = this.isRelationMode ? { name: "updateRelationTarget", relationName: this.statusAttribute, targetNoteId: newValue } - : { name: "updateLabelValue", labelName: this.statusAttribute, labelValue: newValue } + : { name: "updateLabelValue", labelName: this.statusAttribute, labelValue: newValue }; await executeBulkActions(noteIds, [ action ]); // Rename the column in the persisted data. @@ -137,9 +136,9 @@ export default class BoardApi { } async insertRowAtPosition( - column: string, - relativeToBranchId: string, - direction: "before" | "after") { + column: string, + relativeToBranchId: string, + direction: "before" | "after") { const { note, branch } = await note_create.createNote(this.parentNote.noteId, { activate: false, targetBranchId: relativeToBranchId, @@ -179,9 +178,9 @@ export default class BoardApi { if (!note) return; if (this.isRelationMode) { return attributes.removeOwnedRelationByName(note, this.statusAttribute); - } else { - return attributes.removeOwnedLabelByName(note, this.statusAttribute); - } + } + return attributes.removeOwnedLabelByName(note, this.statusAttribute); + } async moveWithinBoard(noteId: string, sourceBranchId: string, sourceIndex: number, targetIndex: number, sourceColumn: string, targetColumn: string) { From 9e99670b19a8d81f79438f7e0623736495eb33fb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 12 Mar 2026 20:53:19 +0200 Subject: [PATCH 6/8] fix(collections): displaying note list even if session is not unlocked --- apps/client/src/components/note_context.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index afcaaf0918..3ca6a2a792 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -381,6 +381,10 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> // Collections must always display a note list, even if no children. if (note.type === "book") { + if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { + return false; + } + const viewType = note.getLabelValue("viewType") ?? "grid"; if (!["list", "grid"].includes(viewType)) { return true; From 9aa84877ee3e6ea6003fa27f05d625213ba54c8c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 12 Mar 2026 21:03:12 +0200 Subject: [PATCH 7/8] fix(tree): not reacting to protected state changes --- apps/client/src/services/froca_updater.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/client/src/services/froca_updater.ts b/apps/client/src/services/froca_updater.ts index 6d6ef9213d..ca6c792746 100644 --- a/apps/client/src/services/froca_updater.ts +++ b/apps/client/src/services/froca_updater.ts @@ -110,7 +110,12 @@ function processNoteChange(loadResults: LoadResults, ec: EntityChange) { } } - if (ec.componentId) { + // Only register as a content change if the protection status didn't change. + // When isProtected changes, the blobId change is a side effect of re-encryption, + // not a content edit. Registering it as content would cause the tree's content-only + // filter to incorrectly skip the note update (since both changes share the same + // componentId). + if (ec.componentId && note.isProtected === (ec.entity as FNoteRow).isProtected) { loadResults.addNoteContent(note.noteId, ec.componentId); } } From b51bfdfb337813ad699682903659ec8d55d14e8f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 12 Mar 2026 21:09:30 +0200 Subject: [PATCH 8/8] chore(client): address requested change --- apps/client/src/widgets/collections/board/api.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/client/src/widgets/collections/board/api.ts b/apps/client/src/widgets/collections/board/api.ts index 16237edd52..525f74d6f1 100644 --- a/apps/client/src/widgets/collections/board/api.ts +++ b/apps/client/src/widgets/collections/board/api.ts @@ -178,9 +178,8 @@ export default class BoardApi { if (!note) return; if (this.isRelationMode) { return attributes.removeOwnedRelationByName(note, this.statusAttribute); - } + } return attributes.removeOwnedLabelByName(note, this.statusAttribute); - } async moveWithinBoard(noteId: string, sourceBranchId: string, sourceIndex: number, targetIndex: number, sourceColumn: string, targetColumn: string) {