From 8a641e1b4f40d9a7120e3d9ce130e49ca6f6573f Mon Sep 17 00:00:00 2001 From: zadam Date: Fri, 6 Jan 2023 20:31:55 +0100 Subject: [PATCH] added "inherit" relation, #3493 --- bin/tpl/anonymize-database.sql | 4 ++-- src/becca/becca.js | 12 ++++++------ src/becca/becca_loader.js | 4 ++-- src/becca/entities/battribute.js | 2 +- src/becca/entities/bnote.js | 16 ++++++++-------- src/public/app/entities/fnote.js | 11 +++++++---- src/public/app/services/attribute_renderer.js | 1 + src/public/app/services/attributes.js | 2 +- src/public/app/services/froca_updater.js | 2 +- .../attribute_widgets/attribute_detail.js | 3 ++- src/public/app/widgets/note_detail.js | 2 +- src/public/app/widgets/note_tree.js | 2 +- src/routes/api/note_map.js | 4 ++-- src/routes/api/tree.js | 2 +- src/services/builtin_attributes.js | 1 + src/services/notes.js | 2 ++ .../search/expressions/attribute_exists.js | 4 ++-- .../search/expressions/label_comparison.js | 4 ++-- .../search/expressions/relation_where.js | 4 ++-- src/share/shaca/entities/sattribute.js | 2 +- src/share/shaca/entities/snote.js | 6 +++--- 21 files changed, 49 insertions(+), 41 deletions(-) diff --git a/bin/tpl/anonymize-database.sql b/bin/tpl/anonymize-database.sql index afdff5168..fca3b4e53 100644 --- a/bin/tpl/anonymize-database.sql +++ b/bin/tpl/anonymize-database.sql @@ -5,8 +5,8 @@ UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL; UPDATE note_revisions SET title = 'title'; UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL; -UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'workspaceInbox', 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteContentChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon'); -UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN ('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'workspaceInbox', 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteContentChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon'); +UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'workspaceInbox', 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteContentChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon'); +UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN ('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'cssClass', 'iconClass', 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'noteRevisionsWidgetDisabled', 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', 'workspaceTabBackgroundColor', 'searchHome', 'workspaceInbox', 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', 'bookmarkFolder', 'sorted', 'top', 'fullContentWidth', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', 'runOnNoteTitleChange', 'runOnNoteContentChange', 'runOnNoteChange', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon'); UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL AND prefix != 'recovered'; UPDATE options SET value = 'anonymized' WHERE name IN ('documentId', 'documentSecret', 'encryptedDataKey', diff --git a/src/becca/becca.js b/src/becca/becca.js index 84d22f807..bb7e14208 100644 --- a/src/becca/becca.js +++ b/src/becca/becca.js @@ -14,17 +14,17 @@ class Becca { reset() { /** @type {Object.} */ this.notes = {}; - /** @type {Object.} */ + /** @type {Object.} */ this.branches = {}; - /** @type {Object.} */ + /** @type {Object.} */ this.childParentToBranch = {}; - /** @type {Object.} */ + /** @type {Object.} */ this.attributes = {}; - /** @type {Object.} Points from attribute type-name to list of attributes */ + /** @type {Object.} Points from attribute type-name to list of attributes */ this.attributeIndex = {}; - /** @type {Object.} */ + /** @type {Object.} */ this.options = {}; - /** @type {Object.} */ + /** @type {Object.} */ this.etapiTokens = {}; this.dirtyNoteSetCache(); diff --git a/src/becca/becca_loader.js b/src/becca/becca_loader.js index c1294cdc7..c5eaa35e2 100644 --- a/src/becca/becca_loader.js +++ b/src/becca/becca_loader.js @@ -187,7 +187,7 @@ function attributeDeleted(attributeId) { if (note) { // first invalidate and only then remove the attribute (otherwise invalidation wouldn't be complete) - if (attribute.isAffectingSubtree || note.isTemplate()) { + if (attribute.isAffectingSubtree || note.isInherited()) { note.invalidateSubTree(); } else { note.invalidateThisCache(); @@ -215,7 +215,7 @@ function attributeUpdated(attribute) { const note = becca.notes[attribute.noteId]; if (note) { - if (attribute.isAffectingSubtree || note.isTemplate()) { + if (attribute.isAffectingSubtree || note.isInherited()) { note.invalidateSubTree(); } else { note.invalidateThisCache(); diff --git a/src/becca/entities/battribute.js b/src/becca/entities/battribute.js index aeae6e808..162729c4e 100644 --- a/src/becca/entities/battribute.js +++ b/src/becca/entities/battribute.js @@ -102,7 +102,7 @@ class BAttribute extends AbstractBeccaEntity { get isAffectingSubtree() { return this.isInheritable - || (this.type === 'relation' && this.name === 'template'); + || (this.type === 'relation' && ['template', 'inherit'].includes(this.name)); } get targetNoteId() { // alias diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 3aa685b95..47cf0599f 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -410,7 +410,7 @@ class BNote extends AbstractBeccaEntity { const templateAttributes = []; for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates - if (ownedAttr.type === 'relation' && ownedAttr.name === 'template') { + if (ownedAttr.type === 'relation' && ['template', 'inherit'].includes(ownedAttr.name)) { const templateNote = this.becca.notes[ownedAttr.value]; if (templateNote) { @@ -805,7 +805,7 @@ class BNote extends AbstractBeccaEntity { } for (const targetRelation of this.targetRelations) { - if (targetRelation.name === 'template') { + if (targetRelation.name === 'template' || targetRelation.name === 'inherit') { const note = targetRelation.note; if (note) { @@ -823,7 +823,7 @@ class BNote extends AbstractBeccaEntity { } for (const targetRelation of this.targetRelations) { - if (targetRelation.name === 'template') { + if (targetRelation.name === 'template' || targetRelation.name === 'inherit') { const note = targetRelation.note; if (note) { @@ -843,8 +843,8 @@ class BNote extends AbstractBeccaEntity { .filter(l => l.name.startsWith("relation:")); } - isTemplate() { - return !!this.targetRelations.find(rel => rel.name === 'template'); + isInherited() { + return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit'); } /** @returns {BNote[]} */ @@ -863,7 +863,7 @@ class BNote extends AbstractBeccaEntity { } for (const targetRelation of note.targetRelations) { - if (targetRelation.name === 'template') { + if (targetRelation.name === 'template' || targetRelation.name === 'inherit') { const targetNote = targetRelation.note; if (targetNote) { @@ -1067,11 +1067,11 @@ class BNote extends AbstractBeccaEntity { /** @returns {BNote[]} - returns only notes which are templated, does not include their subtrees * in effect returns notes which are influenced by note's non-inheritable attributes */ - getTemplatedNotes() { + getInheritingNotes() { const arr = [this]; for (const targetRelation of this.targetRelations) { - if (targetRelation.name === 'template') { + if (targetRelation.name === 'template' || targetRelation.name === 'inherit') { const note = targetRelation.note; if (note) { diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.js index a0207e066..4022c812f 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.js @@ -259,7 +259,7 @@ class FNote { } } - for (const templateAttr of attrArrs.flat().filter(attr => attr.type === 'relation' && attr.name === 'template')) { + for (const templateAttr of attrArrs.flat().filter(attr => attr.type === 'relation' && ['template', 'inherit'].includes(attr.name))) { const templateNote = this.froca.notes[templateAttr.value]; if (templateNote && templateNote.noteId !== this.noteId) { @@ -651,8 +651,11 @@ class FNote { /** * @returns {FNote[]} */ - getTemplateNotes() { - const relations = this.getRelations('template'); + getNotesToInheritAttributesFrom() { + const relations = [ + ...this.getRelations('template'), + ...this.getRelations('inherit') + ]; return relations.map(rel => this.froca.notes[rel.value]); } @@ -690,7 +693,7 @@ class FNote { visitedNoteIds.add(this.noteId); - for (const templateNote of this.getTemplateNotes()) { + for (const templateNote of this.getNotesToInheritAttributesFrom()) { if (templateNote.hasAncestor(ancestorNoteId, visitedNoteIds)) { return true; } diff --git a/src/public/app/services/attribute_renderer.js b/src/public/app/services/attribute_renderer.js index d909bab2c..973a9a841 100644 --- a/src/public/app/services/attribute_renderer.js +++ b/src/public/app/services/attribute_renderer.js @@ -83,6 +83,7 @@ const HIDDEN_ATTRIBUTES = [ 'originalFileName', 'fileSize', 'template', + 'inherit', 'cssClass', 'iconClass', 'pageSize', diff --git a/src/public/app/services/attributes.js b/src/public/app/services/attributes.js index 515d8462f..26dedf467 100644 --- a/src/public/app/services/attributes.js +++ b/src/public/app/services/attributes.js @@ -40,7 +40,7 @@ function isAffecting(attrRow, affectedNote) { return false; } - const owningNotes = [affectedNote, ...affectedNote.getTemplateNotes()]; + const owningNotes = [affectedNote, ...affectedNote.getNotesToInheritAttributesFrom()]; for (const owningNote of owningNotes) { if (owningNote.noteId === attrNote.noteId) { diff --git a/src/public/app/services/froca_updater.js b/src/public/app/services/froca_updater.js index 4fac8e3df..b28f80f3f 100644 --- a/src/public/app/services/froca_updater.js +++ b/src/public/app/services/froca_updater.js @@ -60,7 +60,7 @@ async function processEntityChanges(entityChanges) { } else if (entityName === 'attributes' && entity.type === 'relation' - && entity.name === 'template' + && (entity.name === 'template' || entity.name === 'inherit') && !(entity.value in froca.notes)) { missingNoteIds.push(entity.value); diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.js b/src/public/app/widgets/attribute_widgets/attribute_detail.js index bf44a8a4e..92d7d6b9b 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_detail.js +++ b/src/public/app/widgets/attribute_widgets/attribute_detail.js @@ -254,7 +254,8 @@ const ATTR_HELP = { "runOnBranchDeletion": "executes when a branch is deleted. Branch is a link between parent note and child note and is deleted e.g. when moving note (old branch/link is deleted).", "runOnAttributeCreation": "executes when new attribute is created for the note which defines this relation", "runOnAttributeChange": " executes when the attribute is changed of a note which defines this relation. This is triggered also when the attribute is deleted", - "template": "attached note's attributes will be inherited even without parent-child relationship. See template for details.", + "template": "note's attributes will be inherited even without a parent-child relationship, note's content and subtree will be added to instance notes if empty. See documentation for details.", + "inherit": "note's attributes will be inherited even without a parent-child relationship. See template relation for a similar concept. See attribute inheritance in the documentation.", "renderNote": 'notes of type "render HTML note" will be rendered using a code note (HTML or script) and it is necessary to point using this relation to which note should be rendered', "widget": "target of this relation will be executed and rendered as a widget in the sidebar", "shareCss": "CSS note which will be injected into the share page. CSS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree' and 'shareOmitDefaultCss' as well.", diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js index ab8ea1434..f0d696155 100644 --- a/src/public/app/widgets/note_detail.js +++ b/src/public/app/widgets/note_detail.js @@ -316,7 +316,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { const relation = attrs.find(attr => attr.type === 'relation' - && ['template', 'renderNote'].includes(attr.name) + && ['template', 'inherit', 'renderNote'].includes(attr.name) && attributeService.isAffecting(attr, this.note)); if (label || relation) { diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index c82194708..4f6825c94 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -1117,7 +1117,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } } } - else if (ecAttr.type === 'relation' && ecAttr.name === 'template') { + else if (ecAttr.type === 'relation' && (ecAttr.name === 'template' || ecAttr.name === 'inherit')) { // missing handling of things inherited from template noteIdsToReload.add(ecAttr.noteId); } diff --git a/src/routes/api/note_map.js b/src/routes/api/note_map.js index 91c24a27b..68bc81118 100644 --- a/src/routes/api/note_map.js +++ b/src/routes/api/note_map.js @@ -38,7 +38,7 @@ function getNeighbors(note, depth) { const retNoteIds = []; function isIgnoredRelation(relation) { - return ['relationMapLink', 'template', 'image', 'ancestor'].includes(relation.name); + return ['relationMapLink', 'template', 'inherit', 'image', 'ancestor'].includes(relation.name); } // forward links @@ -126,7 +126,7 @@ function getLinkMap(req) { }); const links = Object.values(becca.attributes).filter(rel => { - if (rel.type !== 'relation' || rel.name === 'relationMapLink' || rel.name === 'template') { + if (rel.type !== 'relation' || rel.name === 'relationMapLink' || rel.name === 'template' || rel.name === 'inherit') { return false; } else if (!noteIds.has(rel.noteId) || !noteIds.has(rel.value)) { diff --git a/src/routes/api/tree.js b/src/routes/api/tree.js index 3f9e386a3..111d882e3 100644 --- a/src/routes/api/tree.js +++ b/src/routes/api/tree.js @@ -32,7 +32,7 @@ function getNotesAndBranchesAndAttributes(noteIds) { for (const attr of note.ownedAttributes) { collectedAttributeIds.add(attr.attributeId); - if (attr.type === 'relation' && attr.name === 'template' && attr.targetNote) { + if (attr.type === 'relation' && ['template', 'inherit'].includes(attr.name) && attr.targetNote) { collectEntityIds(attr.targetNote); } } diff --git a/src/services/builtin_attributes.js b/src/services/builtin_attributes.js index 80331a02a..d3558b884 100644 --- a/src/services/builtin_attributes.js +++ b/src/services/builtin_attributes.js @@ -78,6 +78,7 @@ module.exports = [ { type: 'relation', name: 'runOnAttributeCreation', isDangerous: true }, { type: 'relation', name: 'runOnAttributeChange', isDangerous: true }, { type: 'relation', name: 'template' }, + { type: 'relation', name: 'inherit' }, { type: 'relation', name: 'widget', isDangerous: true }, { type: 'relation', name: 'renderNote', isDangerous: true }, { type: 'relation', name: 'shareCss' }, diff --git a/src/services/notes.js b/src/services/notes.js index ceae44d2c..817f0e0f0 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -197,6 +197,8 @@ function createNewNote(params) { if (!note.hasOwnedRelation('template', params.templateNoteId)) { note.addRelation('template', params.templateNoteId); } + + // no special handling for ~inherit since it doesn't matter if it's assigned with the note creation or later } triggerNoteTitleChanged(note); diff --git a/src/services/search/expressions/attribute_exists.js b/src/services/search/expressions/attribute_exists.js index 9e8e92f00..df4fa938d 100644 --- a/src/services/search/expressions/attribute_exists.js +++ b/src/services/search/expressions/attribute_exists.js @@ -26,10 +26,10 @@ class AttributeExistsExp extends Expression { if (attr.isInheritable) { resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated()); } - else if (note.isTemplate() && + else if (note.isInherited() && // template attr is used as a marker for templates, but it's not meant to be inherited !(this.attributeType === 'label' && (this.attributeName === 'template' || this.attributeName === 'workspacetemplate'))) { - resultNoteSet.addAll(note.getTemplatedNotes()); + resultNoteSet.addAll(note.getInheritingNotes()); } else { resultNoteSet.add(note); diff --git a/src/services/search/expressions/label_comparison.js b/src/services/search/expressions/label_comparison.js index 79cf8cc3e..1d6f7572b 100644 --- a/src/services/search/expressions/label_comparison.js +++ b/src/services/search/expressions/label_comparison.js @@ -25,8 +25,8 @@ class LabelComparisonExp extends Expression { if (attr.isInheritable) { resultNoteSet.addAll(note.getSubtreeNotesIncludingTemplated()); } - else if (note.isTemplate()) { - resultNoteSet.addAll(note.getTemplatedNotes()); + else if (note.isInherited()) { + resultNoteSet.addAll(note.getInheritingNotes()); } else { resultNoteSet.add(note); diff --git a/src/services/search/expressions/relation_where.js b/src/services/search/expressions/relation_where.js index ee9f26081..33d4e8f59 100644 --- a/src/services/search/expressions/relation_where.js +++ b/src/services/search/expressions/relation_where.js @@ -25,8 +25,8 @@ class RelationWhereExp extends Expression { if (subResNoteSet.hasNote(attr.targetNote)) { if (attr.isInheritable) { candidateNoteSet.addAll(note.getSubtreeNotesIncludingTemplated()); - } else if (note.isTemplate()) { - candidateNoteSet.addAll(note.getTemplatedNotes()); + } else if (note.isInherited()) { + candidateNoteSet.addAll(note.getInheritingNotes()); } else { candidateNoteSet.add(note); } diff --git a/src/share/shaca/entities/sattribute.js b/src/share/shaca/entities/sattribute.js index 3f5f03e9e..9e0509aa9 100644 --- a/src/share/shaca/entities/sattribute.js +++ b/src/share/shaca/entities/sattribute.js @@ -56,7 +56,7 @@ class SAttribute extends AbstractShacaEntity { /** @returns {boolean} */ get isAffectingSubtree() { return this.isInheritable - || (this.type === 'relation' && this.name === 'template'); + || (this.type === 'relation' && ['template', 'inherit'].includes(this.name)); } /** @returns {string} */ diff --git a/src/share/shaca/entities/snote.js b/src/share/shaca/entities/snote.js index 56e7a4d70..eb0a96c5a 100644 --- a/src/share/shaca/entities/snote.js +++ b/src/share/shaca/entities/snote.js @@ -167,7 +167,7 @@ class SNote extends AbstractShacaEntity { const templateAttributes = []; for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates - if (ownedAttr.type === 'relation' && ownedAttr.name === 'template') { + if (ownedAttr.type === 'relation' && ['template', 'inherit'].includes(ownedAttr.name)) { const templateNote = this.shaca.notes[ownedAttr.value]; if (templateNote) { @@ -434,8 +434,8 @@ class SNote extends AbstractShacaEntity { } /** @returns {boolean} */ - isTemplate() { - return !!this.targetRelations.find(rel => rel.name === 'template'); + isInherited() { + return !!this.targetRelations.find(rel => rel.name === 'template' || rel.name === 'inherit'); } /** @returns {SAttribute[]} */