mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	fixes in attribute persistence + WIP on display of promoted attrs
This commit is contained in:
		| @@ -9,8 +9,6 @@ class ApiToken extends Entity { | ||||
|     static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; } | ||||
|  | ||||
|     beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         if (!this.isDeleted) { | ||||
|             this.isDeleted = false; | ||||
|         } | ||||
| @@ -18,6 +16,8 @@ class ApiToken extends Entity { | ||||
|         if (!this.dateCreated) { | ||||
|             this.dateCreated = dateUtils.nowDate(); | ||||
|         } | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -10,13 +10,24 @@ class Attribute extends Entity { | ||||
|     static get primaryKeyName() { return "attributeId"; } | ||||
|     static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable", "dateModified", "dateCreated"]; } | ||||
|  | ||||
|     constructor(row) { | ||||
|         super(row); | ||||
|  | ||||
|         try { | ||||
|             this.value = JSON.parse(this.value); | ||||
|         } | ||||
|         catch(e) {} | ||||
|     } | ||||
|  | ||||
|     async getNote() { | ||||
|         return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); | ||||
|     } | ||||
|  | ||||
|     async beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|     isDefinition() { | ||||
|         return this.type === 'label' || this.type === 'relation'; | ||||
|     } | ||||
|  | ||||
|     async beforeSaving() { | ||||
|         if (!this.value) { | ||||
|             // null value isn't allowed | ||||
|             this.value = ""; | ||||
| @@ -39,6 +50,8 @@ class Attribute extends Entity { | ||||
|         } | ||||
|  | ||||
|         this.dateModified = dateUtils.nowDate(); | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -16,8 +16,6 @@ class Branch extends Entity { | ||||
|     } | ||||
|  | ||||
|     async beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         if (this.notePosition === undefined) { | ||||
|             const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]); | ||||
|             this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1; | ||||
| @@ -32,6 +30,8 @@ class Branch extends Entity { | ||||
|         } | ||||
|  | ||||
|         this.dateModified = dateUtils.nowDate(); | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -9,8 +9,6 @@ class Image extends Entity { | ||||
|     static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; } | ||||
|  | ||||
|     beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         if (!this.isDeleted) { | ||||
|             this.isDeleted = false; | ||||
|         } | ||||
| @@ -20,6 +18,8 @@ class Image extends Entity { | ||||
|         } | ||||
|  | ||||
|         this.dateModified = dateUtils.nowDate(); | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -15,8 +15,6 @@ class Label extends Entity { | ||||
|     } | ||||
|  | ||||
|     async beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         if (!this.value) { | ||||
|             // null value isn't allowed | ||||
|             this.value = ""; | ||||
| @@ -35,6 +33,8 @@ class Label extends Entity { | ||||
|         } | ||||
|  | ||||
|         this.dateModified = dateUtils.nowDate(); | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -18,8 +18,6 @@ class NoteImage extends Entity { | ||||
|     } | ||||
|  | ||||
|     beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         if (!this.isDeleted) { | ||||
|             this.isDeleted = false; | ||||
|         } | ||||
| @@ -29,6 +27,8 @@ class NoteImage extends Entity { | ||||
|         } | ||||
|  | ||||
|         this.dateModified = dateUtils.nowDate(); | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -22,11 +22,11 @@ class NoteRevision extends Entity { | ||||
|     } | ||||
|  | ||||
|     beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         if (this.isProtected) { | ||||
|             protectedSessionService.encryptNoteRevision(this); | ||||
|         } | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -9,9 +9,9 @@ class Option extends Entity { | ||||
|     static get hashedProperties() { return ["name", "value"]; } | ||||
|  | ||||
|     beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         this.dateModified = dateUtils.nowDate(); | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -9,8 +9,6 @@ class RecentNote extends Entity { | ||||
|     static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; } | ||||
|  | ||||
|     beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         if (!this.isDeleted) { | ||||
|             this.isDeleted = false; | ||||
|         } | ||||
| @@ -18,6 +16,8 @@ class RecentNote extends Entity { | ||||
|         if (!this.dateCreated) { | ||||
|             this.dateCreated = dateUtils.nowDate(); | ||||
|         } | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -19,8 +19,6 @@ class Relation extends Entity { | ||||
|     } | ||||
|  | ||||
|     async beforeSaving() { | ||||
|         super.beforeSaving(); | ||||
|  | ||||
|         if (this.position === undefined) { | ||||
|             this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM relations WHERE sourceNoteId = ?`, [this.sourceNoteId]); | ||||
|         } | ||||
| @@ -38,6 +36,8 @@ class Relation extends Entity { | ||||
|         } | ||||
|  | ||||
|         this.dateModified = dateUtils.nowDate(); | ||||
|  | ||||
|         super.beforeSaving(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -22,10 +22,10 @@ function AttributesModel() { | ||||
|  | ||||
|     this.availableLabelTypes = [ | ||||
|         { text: "Text", value: "text" }, | ||||
|         { text: "Integer", value: "integer" }, | ||||
|         { text: "Decimal", value: "decimal" }, | ||||
|         { text: "Number", value: "number" }, | ||||
|         { text: "Boolean", value: "boolean" }, | ||||
|         { text: "Date", value: "date" } | ||||
|         { text: "Date", value: "date" }, | ||||
|         { text: "DateTime", value: "datetime" } | ||||
|     ]; | ||||
|  | ||||
|     this.multiplicityTypes = [ | ||||
| @@ -57,15 +57,17 @@ function AttributesModel() { | ||||
|         for (const attr of attributes) { | ||||
|             attr.labelValue = attr.type === 'label' ? attr.value : ''; | ||||
|             attr.relationValue = attr.type === 'relation' ? attr.value : ''; | ||||
|             attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? JSON.parse(attr.value) : { | ||||
|             attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? attr.value : { | ||||
|                 labelType: "text", | ||||
|                 multiplicityType: "singlevalue", | ||||
|                 isPromoted: true | ||||
|             }; | ||||
|             attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? JSON.parse(attr.value) : { | ||||
|             attr.relationDefinition = attr.type === ('relation-definition' && attr.value) ? attr.value : { | ||||
|                 multiplicityType: "singlevalue", | ||||
|                 isPromoted: true | ||||
|             }; | ||||
|  | ||||
|             delete attr.value; | ||||
|         } | ||||
|  | ||||
|         self.attributes(attributes.map(ko.observable)); | ||||
| @@ -140,10 +142,10 @@ function AttributesModel() { | ||||
|                 attr.value = attr.relationValue; | ||||
|             } | ||||
|             else if (attr.type === 'label-definition') { | ||||
|                 attr.value = JSON.stringify(attr.labelDefinition); | ||||
|                 attr.value = attr.labelDefinition; | ||||
|             } | ||||
|             else if (attr.type === 'relation-definition') { | ||||
|                 attr.value = JSON.stringify(attr.relationDefinition); | ||||
|                 attr.value = attr.relationDefinition; | ||||
|             } | ||||
|  | ||||
|             delete attr.labelValue; | ||||
| @@ -158,11 +160,7 @@ function AttributesModel() { | ||||
|  | ||||
|         infoService.showMessage("Attributes have been saved."); | ||||
|  | ||||
|         // FIXME FIXME FIXME FIXME FIXME | ||||
|         // FIXME FIXME FIXME FIXME FIXME | ||||
|         // FIXME FIXME FIXME FIXME FIXME | ||||
|         // FIXME FIXME FIXME FIXME FIXME | ||||
|         //noteDetailService.loadAttributeList(); | ||||
|         noteDetailService.loadAttributes(); | ||||
|     }; | ||||
|  | ||||
|     function addLastEmptyRow() { | ||||
|   | ||||
| @@ -32,6 +32,7 @@ const $relationList = $("#relation-list"); | ||||
| const $relationListInner = $("#relation-list-inner"); | ||||
| const $childrenOverview = $("#children-overview"); | ||||
| const $scriptArea = $("#note-detail-script-area"); | ||||
| const $promotedAttributes = $("#note-detail-promoted-attributes"); | ||||
|  | ||||
| let currentNote = null; | ||||
|  | ||||
| @@ -193,6 +194,8 @@ async function loadNoteDetail(noteId) { | ||||
|     $scriptArea.html(''); | ||||
|  | ||||
|     await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView'); | ||||
|  | ||||
|     await loadAttributes(); | ||||
| } | ||||
|  | ||||
| async function showChildrenOverview(hideChildrenOverview) { | ||||
| @@ -220,6 +223,39 @@ async function showChildrenOverview(hideChildrenOverview) { | ||||
|     $childrenOverview.show(); | ||||
| } | ||||
|  | ||||
| async function loadAttributes() { | ||||
|     $promotedAttributes.empty(); | ||||
|  | ||||
|     const noteId = getCurrentNoteId(); | ||||
|  | ||||
|     const attributes = await server.get('notes/' + noteId + '/attributes'); | ||||
|  | ||||
|     console.log(attributes); | ||||
|  | ||||
|     const promoted = attributes.filter(attr => (attr.type === 'label-definition' || attr.type === 'relation-definition') && attr.value.isPromoted); | ||||
|  | ||||
|     let idx = 1; | ||||
|  | ||||
|     if (promoted.length > 0) { | ||||
|         for (const promotedAttr of promoted) { | ||||
|             if (promotedAttr.type === 'label-definition') { | ||||
|                 const inputId = "promoted-input-" + idx; | ||||
|                 const $div = $("<div>").addClass("class", "form-group"); | ||||
|                 const $label = $("<label>").prop("for", inputId).append(promotedAttr.name); | ||||
|                 const $input = $("<input>") | ||||
|                     .prop("id", inputId) | ||||
|                     .prop("attribute-id", promotedAttr.attributeId) | ||||
|                     .addClass("form-control") | ||||
|                     .addClass("promoted-attribute-input"); | ||||
|  | ||||
|                 $div.append($label).append($input); | ||||
|  | ||||
|                 $promotedAttributes.append($div); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function loadLabelList() { | ||||
|     const noteId = getCurrentNoteId(); | ||||
|  | ||||
| @@ -312,6 +348,7 @@ export default { | ||||
|     getCurrentNoteId, | ||||
|     newNoteCreated, | ||||
|     focus, | ||||
|     loadAttributes, | ||||
|     loadLabelList, | ||||
|     loadRelationList, | ||||
|     saveNote, | ||||
|   | ||||
| @@ -5,10 +5,53 @@ const attributeService = require('../../services/attributes'); | ||||
| const repository = require('../../services/repository'); | ||||
| const Attribute = require('../../entities/attribute'); | ||||
|  | ||||
| async function getNoteAttributes(req) { | ||||
| async function getEffectiveNoteAttributes(req) { | ||||
|     const noteId = req.params.noteId; | ||||
|  | ||||
|     return await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]); | ||||
|     const attributes = await repository.getEntities(` | ||||
|         WITH RECURSIVE tree(noteId, level) AS ( | ||||
|         SELECT ?, 0 | ||||
|             UNION | ||||
|             SELECT branches.parentNoteId, tree.level + 1 FROM branches | ||||
|             JOIN tree ON branches.noteId = tree.noteId | ||||
|             JOIN notes ON notes.noteId = branches.parentNoteId | ||||
|             WHERE notes.isDeleted = 0 AND branches.isDeleted = 0 | ||||
|         ) | ||||
|         SELECT attributes.* FROM attributes JOIN tree ON attributes.noteId = tree.noteId  | ||||
|         WHERE attributes.isDeleted = 0 AND (attributes.isInheritable = 1 OR attributes.noteId = ?) | ||||
|         ORDER BY level, noteId, position`, [noteId, noteId]); | ||||
|         // attributes are ordered so that "closest" attributes are first | ||||
|         // we order by noteId so that attributes from same note stay together. Actual noteId ordering doesn't matter. | ||||
|  | ||||
|     const filteredAttributes = attributes.filter((attr, index) => { | ||||
|         if (attr.isDefinition()) { | ||||
|             const firstDefinitionIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name); | ||||
|  | ||||
|             // keep only if this element is the first definition for this type & name | ||||
|             return firstDefinitionIndex === index; | ||||
|         } | ||||
|         else { | ||||
|             const definitionAttr = attributes.find(el => el.type === attr.type + '-definition' && el.name === attr.name); | ||||
|  | ||||
|             if (!definitionAttr) { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             const definition = definitionAttr.value; | ||||
|  | ||||
|             if (definition.multiplicityType === 'multivalue') { | ||||
|                 return true; | ||||
|             } | ||||
|             else { | ||||
|                 const firstAttrIndex = attributes.findIndex(el => el.type === attr.type && el.name === attr.name); | ||||
|  | ||||
|                 // in case of single-valued attribute we'll keep it only if it's first (closest) | ||||
|                 return firstAttrIndex === index; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     return filteredAttributes; | ||||
| } | ||||
|  | ||||
| async function updateNoteAttributes(req) { | ||||
| @@ -38,6 +81,8 @@ async function updateNoteAttributes(req) { | ||||
|         attributeEntity.isInheritable = attribute.isInheritable; | ||||
|         attributeEntity.isDeleted = attribute.isDeleted; | ||||
|  | ||||
|         console.log("ATTR: ", attributeEntity); | ||||
|  | ||||
|         await attributeEntity.save(); | ||||
|     } | ||||
|  | ||||
| @@ -58,8 +103,8 @@ async function getValuesForAttribute(req) { | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     getNoteAttributes, | ||||
|     updateNoteAttributes, | ||||
|     getAttributeNames, | ||||
|     getValuesForAttribute | ||||
|     getValuesForAttribute, | ||||
|     getEffectiveNoteAttributes | ||||
| }; | ||||
| @@ -134,7 +134,7 @@ function register(app) { | ||||
|  | ||||
|     route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); | ||||
|  | ||||
|     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getNoteAttributes); | ||||
|     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes); | ||||
|     apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames); | ||||
|     apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); | ||||
|   | ||||
| @@ -62,6 +62,12 @@ async function updateEntity(entity) { | ||||
|  | ||||
|     delete clone.jsonContent; | ||||
|  | ||||
|     for (const key in clone) { | ||||
|         if (clone[key] !== null && typeof clone[key] === 'object') { | ||||
|             clone[key] = JSON.stringify(clone[key]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     await sql.transactional(async () => { | ||||
|         await sql.replace(entity.constructor.tableName, clone); | ||||
|  | ||||
|   | ||||
| @@ -183,6 +183,8 @@ | ||||
|       <div id="note-detail-wrapper"> | ||||
|         <div id="note-detail-script-area"></div> | ||||
|  | ||||
|         <div id="note-detail-promoted-attributes"></div> | ||||
|  | ||||
|         <div id="note-detail-component-wrapper"> | ||||
|           <div id="note-detail-text" class="note-detail-component" tabindex="2"></div> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user