import { AttributeType } from "@triliumnext/commons"; import type FNote from "../entities/fnote.js"; import froca from "./froca.js"; import type { AttributeRow } from "./load_results.js"; import server from "./server.js"; async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) { await server.put(`notes/${noteId}/attribute`, { type: "label", name, value, isInheritable }); } export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false, componentId?: string) { await server.put(`notes/${noteId}/set-attribute`, { type: "label", name, value, isInheritable, }, componentId); } export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) { await server.put(`notes/${noteId}/set-attribute`, { type: "relation", name, value, isInheritable }); } /** * Sets a boolean label on the given note, taking inheritance into account. If the desired value matches the inherited * value, any owned label will be removed to allow the inherited value to take effect. If the desired value differs * from the inherited value, an owned label will be created or updated to reflect the desired value. * * When checking if the boolean value is set, don't use `note.hasLabel`; instead use `note.isLabelTruthy`. * * @param note the note on which to set the boolean label. * @param labelName the name of the label to set. * @param value the boolean value to set for the label. */ export async function setBooleanWithInheritance(note: FNote, labelName: string, value: boolean) { const actualValue = note.isLabelTruthy(labelName); if (actualValue === value) return; const hasInheritedValue = !note.hasOwnedLabel(labelName) && note.hasLabel(labelName); if (hasInheritedValue) { if (value) { setLabel(note.noteId, labelName, ""); } else { // Label is inherited - override to false. setLabel(note.noteId, labelName, "false"); } } else if (value) { setLabel(note.noteId, labelName, ""); } else { removeOwnedLabelByName(note, labelName); } } async function removeAttributeById(noteId: string, attributeId: string) { await server.remove(`notes/${noteId}/attributes/${attributeId}`); } export async function removeOwnedAttributesByNameOrType(note: FNote, type: AttributeType, name: string) { for (const attr of note.getOwnedAttributes()) { if (attr.type === type && attr.name === name) { await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`); } } } /** * Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e. * it will not remove inherited attributes. * * @param note the note from which to remove the label. * @param labelName the name of the label to remove. * @returns `true` if an attribute was identified and removed, `false` otherwise. */ function removeOwnedLabelByName(note: FNote, labelName: string) { const label = note.getOwnedLabel(labelName); if (label) { removeAttributeById(note.noteId, label.attributeId); return true; } return false; } /** * Removes a relation identified by its name from the given note, if it exists. Note that the relation must be owned, i.e. * it will not remove inherited attributes. * * @param note the note from which to remove the relation. * @param relationName the name of the relation to remove. * @returns `true` if an attribute was identified and removed, `false` otherwise. */ function removeOwnedRelationByName(note: FNote, relationName: string) { const relation = note.getOwnedRelation(relationName); if (relation) { removeAttributeById(note.noteId, relation.attributeId); return true; } return false; } /** * Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy. * For an attribute with an empty value, pass an empty string instead. * * @param note the note to set the attribute to. * @param type the type of attribute (label or relation). * @param name the name of the attribute to set. * @param value the value of the attribute to set. */ export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined, componentId?: string) { if (value !== null && value !== undefined) { // Create or update the attribute. await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }, componentId); } else { // Remove the attribute if it exists on the server but we don't define a value for it. const attributeId = note.getAttribute(type, name)?.attributeId; if (attributeId) { await server.remove(`notes/${note.noteId}/attributes/${attributeId}`, componentId); } } } /** * @returns - returns true if this attribute has the potential to influence the note in the argument. * That can happen in multiple ways: * 1. attribute is owned by the note * 2. attribute is owned by the template of the note * 3. attribute is owned by some note's ancestor and is inheritable */ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefined) { if (!affectedNote || !attrRow) { return false; } const attrNote = attrRow.noteId && froca.notes[attrRow.noteId]; if (!attrNote) { // the note (owner of the attribute) is not even loaded into the cache, so it should not affect anything else return false; } const owningNotes = [affectedNote, ...affectedNote.getNotesToInheritAttributesFrom()]; for (const owningNote of owningNotes) { if (owningNote.noteId === attrNote.noteId) { return true; } } if (attrRow.isInheritable) { for (const owningNote of owningNotes) { if (owningNote.hasAncestor(attrNote.noteId, true)) { return true; } } } return false; } /** * Toggles whether a dangerous attribute is enabled or not. When an attribute is disabled, its name is prefixed with `disabled:`. * * Note that this work for non-dangerous attributes as well. * * If there are multiple attributes with the same name, all of them will be toggled at the same time. * * @param note the note whose attribute to change. * @param type the type of dangerous attribute (label or relation). * @param name the name of the dangerous attribute. * @param willEnable whether to enable or disable the attribute. * @returns a promise that will resolve when the request to the server completes. */ async function toggleDangerousAttribute(note: FNote, type: "label" | "relation", name: string, willEnable: boolean) { const attrs = [ ...note.getOwnedAttributes(type, name), ...note.getOwnedAttributes(type, `disabled:${name}`) ]; for (const attr of attrs) { const baseName = getNameWithoutDangerousPrefix(attr.name); const newName = willEnable ? baseName : `disabled:${baseName}`; if (newName === attr.name) continue; // We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically. if (attr.type === "label") { await setLabel(note.noteId, newName, attr.value); } else { await setRelation(note.noteId, newName, attr.value); } await removeAttributeById(note.noteId, attr.attributeId); } } /** * Returns the name of an attribute without the `disabled:` prefix, or the same name if it's not disabled. * @param name the name of an attribute. * @returns the name without the `disabled:` prefix. */ function getNameWithoutDangerousPrefix(name: string) { return name.startsWith("disabled:") ? name.substring(9) : name; } export default { addLabel, setLabel, setRelation, setAttribute, setBooleanWithInheritance, removeAttributeById, removeOwnedLabelByName, removeOwnedRelationByName, isAffecting, toggleDangerousAttribute, getNameWithoutDangerousPrefix };