diff --git a/src/entities/attribute.js b/src/entities/attribute.js index 547f96a38..38d92cd06 100644 --- a/src/entities/attribute.js +++ b/src/entities/attribute.js @@ -13,6 +13,8 @@ class Attribute extends Entity { constructor(row) { super(row); + this.isInheritable = !!this.isInheritable; + if (this.isDefinition()) { try { this.value = JSON.parse(this.value); diff --git a/src/entities/entity.js b/src/entities/entity.js index 8908ae6c2..18200118d 100644 --- a/src/entities/entity.js +++ b/src/entities/entity.js @@ -7,6 +7,10 @@ class Entity { for (const key in row) { this[key] = row[key]; } + + if ('isDeleted' in this) { + this.isDeleted = !!this.isDeleted; + } } beforeSaving() { diff --git a/src/entities/note.js b/src/entities/note.js index 0aaa094bc..0c2b4998e 100644 --- a/src/entities/note.js +++ b/src/entities/note.js @@ -13,6 +13,8 @@ class Note extends Entity { constructor(row) { super(row); + this.isProtected = !!this.isProtected; + // check if there's noteId, otherwise this is a new entity which wasn't encrypted yet if (this.isProtected && this.noteId) { protectedSessionService.decryptNote(this); diff --git a/src/entities/note_revision.js b/src/entities/note_revision.js index 462d6176b..b50fcd38a 100644 --- a/src/entities/note_revision.js +++ b/src/entities/note_revision.js @@ -7,11 +7,13 @@ const repository = require('../services/repository'); class NoteRevision extends Entity { static get tableName() { return "note_revisions"; } static get primaryKeyName() { return "noteRevisionId"; } - static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; } + static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "isProtected", "dateModifiedFrom", "dateModifiedTo"]; } constructor(row) { super(row); + this.isProtected = !!this.isProtected; + if (this.isProtected) { protectedSessionService.decryptNoteRevision(this); } diff --git a/src/entities/option.js b/src/entities/option.js index f4ed9d1f9..7176bfbd9 100644 --- a/src/entities/option.js +++ b/src/entities/option.js @@ -8,6 +8,12 @@ class Option extends Entity { static get primaryKeyName() { return "name"; } static get hashedProperties() { return ["name", "value"]; } + constructor(row) { + super(row); + + this.isSynced = !!this.isSynced; + } + beforeSaving() { this.dateModified = dateUtils.nowDate(); diff --git a/src/public/javascripts/dialogs/attributes.js b/src/public/javascripts/dialogs/attributes.js index 7671e7333..d8a8fad08 100644 --- a/src/public/javascripts/dialogs/attributes.js +++ b/src/public/javascripts/dialogs/attributes.js @@ -3,18 +3,18 @@ import server from '../services/server.js'; import infoService from "../services/info.js"; import treeUtils from "../services/tree_utils.js"; import linkService from "../services/link.js"; -import noteAutocompleteService from "../services/note_autocomplete.js"; const $dialog = $("#attributes-dialog"); const $saveAttributesButton = $("#save-attributes-button"); -const $attributesBody = $('#attributes-table tbody'); +const $ownedAttributesBody = $('#owned-attributes-table tbody'); const attributesModel = new AttributesModel(); function AttributesModel() { const self = this; - this.attributes = ko.observableArray(); + this.ownedAttributes = ko.observableArray(); + this.inheritedAttributes = ko.observableArray(); this.availableTypes = [ { text: "Label", value: "label" }, @@ -47,8 +47,8 @@ function AttributesModel() { let position = 0; // we need to update positions by searching in the DOM, because order of the - // attributes in the viewmodel (self.attributes()) stays the same - $attributesBody.find('input[name="position"]').each(function() { + // attributes in the viewmodel (self.ownedAttributes()) stays the same + $ownedAttributesBody.find('input[name="position"]').each(function() { const attribute = self.getTargetAttribute(this); attribute().position = position++; @@ -56,7 +56,9 @@ function AttributesModel() { }; async function showAttributes(attributes) { - for (const attr of attributes) { + const ownedAttributes = attributes.filter(attr => attr.isOwned); + + for (const attr of ownedAttributes) { attr.labelValue = attr.type === 'label' ? attr.value : ''; attr.relationValue = attr.type === 'relation' ? (await treeUtils.getNoteTitle(attr.value) + " (" + attr.value + ")") : ''; attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? attr.value : { @@ -72,9 +74,13 @@ function AttributesModel() { delete attr.value; } - self.attributes(attributes.map(ko.observable)); + self.ownedAttributes(ownedAttributes.map(ko.observable)); addLastEmptyRow(); + + const inheritedAttributes = attributes.filter(attr => !attr.isOwned); + + self.inheritedAttributes(inheritedAttributes); } this.loadAttributes = async function() { @@ -87,9 +93,9 @@ function AttributesModel() { // attribute might not be rendered immediatelly so could not focus setTimeout(() => $(".attribute-name:last").focus(), 100); - $attributesBody.sortable({ + $ownedAttributesBody.sortable({ handle: '.handle', - containment: $attributesBody, + containment: $ownedAttributesBody, update: this.updateAttributePositions }); }; @@ -99,7 +105,7 @@ function AttributesModel() { const attributeData = attribute(); if (attributeData) { - attributeData.isDeleted = 1; + attributeData.isDeleted = true; attribute(attributeData); @@ -108,7 +114,7 @@ function AttributesModel() { }; function isValid() { - for (let attributes = self.attributes(), i = 0; i < attributes.length; i++) { + for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) { if (self.isEmptyName(i)) { return false; } @@ -132,7 +138,7 @@ function AttributesModel() { const noteId = noteDetailService.getCurrentNoteId(); - const attributesToSave = self.attributes() + const attributesToSave = self.ownedAttributes() .map(attribute => attribute()) .filter(attribute => attribute.attributeId !== "" || attribute.name !== ""); @@ -166,18 +172,18 @@ function AttributesModel() { }; function addLastEmptyRow() { - const attributes = self.attributes().filter(attr => attr().isDeleted === 0); + const attributes = self.ownedAttributes().filter(attr => !attr().isDeleted); const last = attributes.length === 0 ? null : attributes[attributes.length - 1](); if (!last || last.name.trim() !== "") { - self.attributes.push(ko.observable({ + self.ownedAttributes.push(ko.observable({ attributeId: '', type: 'label', name: '', labelValue: '', relationValue: '', isInheritable: false, - isDeleted: 0, + isDeleted: false, position: 0, labelDefinition: { labelType: "text", @@ -201,13 +207,13 @@ function AttributesModel() { }; this.isNotUnique = function(index) { - const cur = self.attributes()[index](); + const cur = self.ownedAttributes()[index](); if (cur.name.trim() === "") { return false; } - for (let attributes = self.attributes(), i = 0; i < attributes.length; i++) { + for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) { const attribute = attributes[i](); if (index !== i && cur.name === attribute.name && cur.type === attribute.type) { @@ -219,7 +225,7 @@ function AttributesModel() { }; this.isEmptyName = function(index) { - const cur = self.attributes()[index](); + const cur = self.ownedAttributes()[index](); return cur.name.trim() === "" && (cur.attributeId !== "" || cur.labelValue !== "" || cur.relationValue); }; @@ -228,7 +234,7 @@ function AttributesModel() { const context = ko.contextFor(target); const index = context.$index(); - return self.attributes()[index]; + return self.ownedAttributes()[index]; } } diff --git a/src/public/javascripts/dialogs/labels.js b/src/public/javascripts/dialogs/labels.js index 2b39eb924..3a68ca71b 100644 --- a/src/public/javascripts/dialogs/labels.js +++ b/src/public/javascripts/dialogs/labels.js @@ -52,7 +52,7 @@ function LabelsModel() { const labelData = label(); if (labelData) { - labelData.isDeleted = 1; + labelData.isDeleted = true; label(labelData); @@ -101,7 +101,7 @@ function LabelsModel() { }; function addLastEmptyRow() { - const labels = self.labels().filter(attr => attr().isDeleted === 0); + const labels = self.labels().filter(attr => !attr().isDeleted); const last = labels.length === 0 ? null : labels[labels.length - 1](); if (!last || last.name.trim() !== "" || last.value !== "") { @@ -109,7 +109,7 @@ function LabelsModel() { labelId: '', name: '', value: '', - isDeleted: 0, + isDeleted: false, position: 0 })); } diff --git a/src/public/javascripts/dialogs/relations.js b/src/public/javascripts/dialogs/relations.js index 62dc212c0..97bfad225 100644 --- a/src/public/javascripts/dialogs/relations.js +++ b/src/public/javascripts/dialogs/relations.js @@ -62,7 +62,7 @@ function RelationsModel() { const relationData = relation(); if (relationData) { - relationData.isDeleted = 1; + relationData.isDeleted = true; relation(relationData); @@ -115,7 +115,7 @@ function RelationsModel() { }; function addLastEmptyRow() { - const relations = self.relations().filter(attr => attr().isDeleted === 0); + const relations = self.relations().filter(attr => !attr().isDeleted); const last = relations.length === 0 ? null : relations[relations.length - 1](); if (!last || last.name.trim() !== "" || last.targetNoteId !== "") { @@ -123,8 +123,8 @@ function RelationsModel() { relationId: '', name: '', targetNoteId: '', - isInheritable: 0, - isDeleted: 0, + isInheritable: false, + isDeleted: false, position: 0 })); } diff --git a/src/public/javascripts/services/link.js b/src/public/javascripts/services/link.js index 3afb5b24b..cae6ccf5b 100644 --- a/src/public/javascripts/services/link.js +++ b/src/public/javascripts/services/link.js @@ -23,7 +23,7 @@ function getNotePathFromLabel(label) { return null; } -async function createNoteLink(notePath, noteTitle) { +async function createNoteLink(notePath, noteTitle = null) { if (!noteTitle) { const noteId = treeUtils.getNoteIdFromNotePath(notePath); @@ -90,6 +90,18 @@ function addTextToEditor(text) { doc.enqueueChanges(() => editor.data.insertText(text), doc.selection); } +ko.bindingHandlers.noteLink = { + init: async function(element, valueAccessor, allBindings, viewModel, bindingContext) { + const noteId = ko.unwrap(valueAccessor()); + + if (noteId) { + const link = await createNoteLink(noteId); + + $(element).append(link); + } + } +}; + // when click on link popup, in case of internal link, just go the the referenced note instead of default behavior // of opening the link in new window/tab $(document).on('click', "a[action='note']", goToLink); diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index fddbfba9d..e4ab7256a 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -228,6 +228,7 @@ async function showChildrenOverview(hideChildrenOverview) { async function loadAttributes() { $promotedAttributesContainer.empty(); + $attributeList.hide(); const noteId = getCurrentNoteId(); @@ -244,7 +245,7 @@ async function loadAttributes() { const $labelCell = $("").append(valueAttr.name); const $input = $("") .prop("id", inputId) - .prop("attribute-id", valueAttr.attributeId) + .prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one .prop("attribute-type", valueAttr.type) .prop("attribute-name", valueAttr.name) .prop("value", valueAttr.value) @@ -409,9 +410,6 @@ async function loadAttributes() { $attributeList.show(); } - else { - $attributeList.hide(); - } } } diff --git a/src/public/javascripts/services/tooltip.js b/src/public/javascripts/services/tooltip.js index 706afad87..43c42693c 100644 --- a/src/public/javascripts/services/tooltip.js +++ b/src/public/javascripts/services/tooltip.js @@ -4,7 +4,7 @@ import linkService from "./link.js"; function setupTooltip() { $(document).tooltip({ - items: "#note-detail-wrapper a", + items: "body a", content: function (callback) { let notePath = linkService.getNotePathFromLink($(this).attr("href")); diff --git a/src/routes/api/attributes.js b/src/routes/api/attributes.js index 50cd05801..2af04929f 100644 --- a/src/routes/api/attributes.js +++ b/src/routes/api/attributes.js @@ -51,6 +51,10 @@ async function getEffectiveNoteAttributes(req) { } }); + for (const attr of filteredAttributes) { + attr.isOwned = attr.noteId === noteId; + } + return filteredAttributes; } @@ -70,7 +74,7 @@ async function updateNoteAttribute(req) { } if (attribute.noteId !== noteId) { - throw new Error(`Attribute ${body.attributeId} does not belong to note ${noteId}`); + return [400, `Attribute ${body.attributeId} is not owned by ${noteId}`]; } attribute.value = body.value; @@ -83,12 +87,17 @@ async function updateNoteAttribute(req) { } async function deleteNoteAttribute(req) { + const noteId = req.params.noteId; const attributeId = req.params.attributeId; const attribute = await repository.getAttribute(attributeId); if (attribute) { - attribute.isDeleted = 1; + if (attribute.noteId !== noteId) { + return [400, `Attribute ${attributeId} is not owned by ${noteId}`]; + } + + attribute.isDeleted = true; await attribute.save(); } } @@ -102,6 +111,10 @@ async function updateNoteAttributes(req) { if (attribute.attributeId) { attributeEntity = await repository.getAttribute(attribute.attributeId); + + if (attributeEntity.noteId !== noteId) { + return [400, `Attribute ${attributeEntity.noteId} is not owned by ${noteId}`]; + } } else { // if it was "created" and then immediatelly deleted, we just don't create it at all @@ -120,12 +133,10 @@ async function updateNoteAttributes(req) { attributeEntity.isInheritable = attribute.isInheritable; attributeEntity.isDeleted = attribute.isDeleted; - console.log("ATTR: ", attributeEntity); - await attributeEntity.save(); } - return await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]); + return await getEffectiveNoteAttributes(req); } async function getAttributeNames(req) { diff --git a/src/services/notes.js b/src/services/notes.js index b32b658cb..14fdf413d 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -192,7 +192,7 @@ async function saveNoteRevision(note) { content: note.content, type: note.type, mime: note.mime, - isProtected: 0, // will be fixed in the protectNoteRevisions() call + isProtected: false, // will be fixed in the protectNoteRevisions() call dateModifiedFrom: note.dateModified, dateModifiedTo: dateUtils.nowDate() }).save(); @@ -226,7 +226,7 @@ async function updateNote(noteId, noteUpdates) { } async function deleteNote(branch) { - if (!branch || branch.isDeleted === 1) { + if (!branch || branch.isDeleted) { return; } diff --git a/src/views/index.ejs b/src/views/index.ejs index 9740109df..5052bcfbb 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -566,7 +566,7 @@
- +
@@ -577,8 +577,8 @@ - - + +
@@ -624,6 +624,45 @@
+ +
+

Inherited attributes

+ + + + + + + + + + + + + + + + + + +
TypeNameValueOwning note
+ + + + + + + + + + promoted: + + + + promoted: + +
+
@@ -646,7 +685,7 @@ - + @@ -691,7 +730,7 @@ - +