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
+
+
+
+
+ Type |
+ Name |
+ Value |
+ Owning note |
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+ promoted:
+
+
+
+ promoted:
+
+ | |
+
+
+
+
+
@@ -646,7 +685,7 @@
-
+
@@ -691,7 +730,7 @@
|
-
+
| |