mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
cleanup of labels & relations frontend code
This commit is contained in:
parent
5f36856571
commit
3491235533
@ -1,222 +0,0 @@
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
|
||||
const $dialog = $("#labels-dialog");
|
||||
const $saveLabelsButton = $("#save-labels-button");
|
||||
const $labelsBody = $('#labels-table tbody');
|
||||
|
||||
const labelsModel = new LabelsModel();
|
||||
let labelNames = [];
|
||||
|
||||
function LabelsModel() {
|
||||
const self = this;
|
||||
|
||||
this.labels = ko.observableArray();
|
||||
|
||||
this.updateLabelPositions = function() {
|
||||
let position = 0;
|
||||
|
||||
// we need to update positions by searching in the DOM, because order of the
|
||||
// labels in the viewmodel (self.labels()) stays the same
|
||||
$labelsBody.find('input[name="position"]').each(function() {
|
||||
const label = self.getTargetLabel(this);
|
||||
|
||||
label().position = position++;
|
||||
});
|
||||
};
|
||||
|
||||
this.loadLabels = async function() {
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const labels = await server.get('notes/' + noteId + '/labels');
|
||||
|
||||
self.labels(labels.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
labelNames = await server.get('labels/names');
|
||||
|
||||
// label might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".label-name:last").focus(), 100);
|
||||
|
||||
$labelsBody.sortable({
|
||||
handle: '.handle',
|
||||
containment: $labelsBody,
|
||||
update: this.updateLabelPositions
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteLabel = function(data, event) {
|
||||
const label = self.getTargetLabel(event.target);
|
||||
const labelData = label();
|
||||
|
||||
if (labelData) {
|
||||
labelData.isDeleted = true;
|
||||
|
||||
label(labelData);
|
||||
|
||||
addLastEmptyRow();
|
||||
}
|
||||
};
|
||||
|
||||
function isValid() {
|
||||
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
|
||||
if (self.isEmptyName(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.save = async function() {
|
||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||
$saveLabelsButton.focus();
|
||||
|
||||
if (!isValid()) {
|
||||
alert("Please fix all validation errors and try saving again.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.updateLabelPositions();
|
||||
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const labelsToSave = self.labels()
|
||||
.map(label => label())
|
||||
.filter(label => label.labelId !== "" || label.name !== "");
|
||||
|
||||
const labels = await server.put('notes/' + noteId + '/labels', labelsToSave);
|
||||
|
||||
self.labels(labels.map(ko.observable));
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
infoService.showMessage("Labels have been saved.");
|
||||
|
||||
noteDetailService.loadLabelList();
|
||||
};
|
||||
|
||||
function addLastEmptyRow() {
|
||||
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 !== "") {
|
||||
self.labels.push(ko.observable({
|
||||
labelId: '',
|
||||
name: '',
|
||||
value: '',
|
||||
isDeleted: false,
|
||||
position: 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.labelChanged = function (data, event) {
|
||||
addLastEmptyRow();
|
||||
|
||||
const label = self.getTargetLabel(event.target);
|
||||
|
||||
label.valueHasMutated();
|
||||
};
|
||||
|
||||
this.isNotUnique = function(index) {
|
||||
const cur = self.labels()[index]();
|
||||
|
||||
if (cur.name.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
|
||||
const label = labels[i]();
|
||||
|
||||
if (index !== i && cur.name === label.name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.isEmptyName = function(index) {
|
||||
const cur = self.labels()[index]();
|
||||
|
||||
return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== "");
|
||||
};
|
||||
|
||||
this.getTargetLabel = function(target) {
|
||||
const context = ko.contextFor(target);
|
||||
const index = context.$index();
|
||||
|
||||
return self.labels()[index];
|
||||
}
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
await labelsModel.loadLabels();
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 800,
|
||||
height: 500
|
||||
});
|
||||
}
|
||||
|
||||
ko.applyBindings(labelsModel, $dialog[0]);
|
||||
|
||||
$dialog.on('focus', '.label-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: labelNames.map(label => {
|
||||
return {
|
||||
label: label,
|
||||
value: label
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
$dialog.on('focus', '.label-value', async function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
const labelName = $(this).parent().parent().find('.label-name').val();
|
||||
|
||||
if (labelName.trim() === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName));
|
||||
|
||||
if (labelValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: labelValues.map(label => {
|
||||
return {
|
||||
label: label,
|
||||
value: label
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
@ -1,250 +0,0 @@
|
||||
import noteDetailService from '../services/note_detail.js';
|
||||
import server from '../services/server.js';
|
||||
import infoService from "../services/info.js";
|
||||
import linkService from "../services/link.js";
|
||||
import treeUtils from "../services/tree_utils.js";
|
||||
|
||||
const $dialog = $("#relations-dialog");
|
||||
const $saveRelationsButton = $("#save-relations-button");
|
||||
const $relationsBody = $('#relations-table tbody');
|
||||
|
||||
const relationsModel = new RelationsModel();
|
||||
let relationNames = [];
|
||||
|
||||
function RelationsModel() {
|
||||
const self = this;
|
||||
|
||||
this.relations = ko.observableArray();
|
||||
|
||||
this.updateRelationPositions = function() {
|
||||
let position = 0;
|
||||
|
||||
// we need to update positions by searching in the DOM, because order of the
|
||||
// relations in the viewmodel (self.relations()) stays the same
|
||||
$relationsBody.find('input[name="position"]').each(function() {
|
||||
const relation = self.getTargetRelation(this);
|
||||
|
||||
relation().position = position++;
|
||||
});
|
||||
};
|
||||
|
||||
async function showRelations(relations) {
|
||||
for (const relation of relations) {
|
||||
relation.targetNoteId = await treeUtils.getNoteTitle(relation.targetNoteId) + " (" + relation.targetNoteId + ")";
|
||||
}
|
||||
|
||||
self.relations(relations.map(ko.observable));
|
||||
}
|
||||
|
||||
this.loadRelations = async function() {
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const relations = await server.get('notes/' + noteId + '/relations');
|
||||
|
||||
await showRelations(relations);
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
relationNames = await server.get('relations/names');
|
||||
|
||||
// relation might not be rendered immediatelly so could not focus
|
||||
setTimeout(() => $(".relation-name:last").focus(), 100);
|
||||
|
||||
$relationsBody.sortable({
|
||||
handle: '.handle',
|
||||
containment: $relationsBody,
|
||||
update: this.updateRelationPositions
|
||||
});
|
||||
};
|
||||
|
||||
this.deleteRelation = function(data, event) {
|
||||
const relation = self.getTargetRelation(event.target);
|
||||
const relationData = relation();
|
||||
|
||||
if (relationData) {
|
||||
relationData.isDeleted = true;
|
||||
|
||||
relation(relationData);
|
||||
|
||||
addLastEmptyRow();
|
||||
}
|
||||
};
|
||||
|
||||
function isValid() {
|
||||
for (let relations = self.relations(), i = 0; i < relations.length; i++) {
|
||||
if (self.isEmptyName(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.save = async function() {
|
||||
// we need to defocus from input (in case of enter-triggered save) because value is updated
|
||||
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
|
||||
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
|
||||
$saveRelationsButton.focus();
|
||||
|
||||
if (!isValid()) {
|
||||
alert("Please fix all validation errors and try saving again.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.updateRelationPositions();
|
||||
|
||||
const noteId = noteDetailService.getCurrentNoteId();
|
||||
|
||||
const relationsToSave = self.relations()
|
||||
.map(relation => relation())
|
||||
.filter(relation => relation.relationId !== "" || relation.name !== "");
|
||||
|
||||
relationsToSave.forEach(relation => relation.targetNoteId = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(relation.targetNoteId)));
|
||||
|
||||
console.log(relationsToSave);
|
||||
|
||||
const relations = await server.put('notes/' + noteId + '/relations', relationsToSave);
|
||||
|
||||
await showRelations(relations);
|
||||
|
||||
addLastEmptyRow();
|
||||
|
||||
infoService.showMessage("Relations have been saved.");
|
||||
|
||||
noteDetailService.loadRelationList();
|
||||
};
|
||||
|
||||
function addLastEmptyRow() {
|
||||
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 !== "") {
|
||||
self.relations.push(ko.observable({
|
||||
relationId: '',
|
||||
name: '',
|
||||
targetNoteId: '',
|
||||
isInheritable: false,
|
||||
isDeleted: false,
|
||||
position: 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
this.relationChanged = function (data, event) {
|
||||
addLastEmptyRow();
|
||||
|
||||
const relation = self.getTargetRelation(event.target);
|
||||
|
||||
relation.valueHasMutated();
|
||||
};
|
||||
|
||||
this.isNotUnique = function(index) {
|
||||
const cur = self.relations()[index]();
|
||||
|
||||
if (cur.name.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let relations = self.relations(), i = 0; i < relations.length; i++) {
|
||||
const relation = relations[i]();
|
||||
|
||||
if (index !== i && cur.name === relation.name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.isEmptyName = function(index) {
|
||||
const cur = self.relations()[index]();
|
||||
|
||||
return cur.name.trim() === "" && (cur.relationId !== "" || cur.targetNoteId !== "");
|
||||
};
|
||||
|
||||
this.getTargetRelation = function(target) {
|
||||
const context = ko.contextFor(target);
|
||||
const index = context.$index();
|
||||
|
||||
return self.relations()[index];
|
||||
}
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
await relationsModel.loadRelations();
|
||||
|
||||
$dialog.dialog({
|
||||
modal: true,
|
||||
width: 900,
|
||||
height: 500
|
||||
});
|
||||
}
|
||||
|
||||
ko.applyBindings(relationsModel, document.getElementById('relations-dialog'));
|
||||
|
||||
$dialog.on('focus', '.relation-name', function (e) {
|
||||
if (!$(this).hasClass("ui-autocomplete-input")) {
|
||||
$(this).autocomplete({
|
||||
// shouldn't be required and autocomplete should just accept array of strings, but that fails
|
||||
// because we have overriden filter() function in autocomplete.js
|
||||
source: relationNames.map(relation => {
|
||||
return {
|
||||
label: relation,
|
||||
value: relation
|
||||
}
|
||||
}),
|
||||
minLength: 0
|
||||
});
|
||||
}
|
||||
|
||||
$(this).autocomplete("search", $(this).val());
|
||||
});
|
||||
|
||||
async function initNoteAutocomplete($el) {
|
||||
if (!$el.hasClass("ui-autocomplete-input")) {
|
||||
await $el.autocomplete({
|
||||
source: async function (request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
|
||||
if (result.length > 0) {
|
||||
response(result.map(row => {
|
||||
return {
|
||||
label: row.label,
|
||||
value: row.label + ' (' + row.value + ')'
|
||||
}
|
||||
}));
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
minLength: 0,
|
||||
select: function (event, ui) {
|
||||
if (ui.item.value === 'No results') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$dialog.on('focus', '.relation-target-note-id', async function () {
|
||||
await initNoteAutocomplete($(this));
|
||||
});
|
||||
|
||||
$dialog.on('click', '.relations-show-recent-notes', async function () {
|
||||
const $autocomplete = $(this).parent().find('.relation-target-note-id');
|
||||
|
||||
await initNoteAutocomplete($autocomplete);
|
||||
|
||||
$autocomplete.autocomplete("search", "");
|
||||
});
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
1
src/public/javascripts/services/bootstrap.js
vendored
1
src/public/javascripts/services/bootstrap.js
vendored
@ -1,6 +1,5 @@
|
||||
import addLinkDialog from '../dialogs/add_link.js';
|
||||
import jumpToNoteDialog from '../dialogs/jump_to_note.js';
|
||||
import labelsDialog from '../dialogs/labels.js';
|
||||
import attributesDialog from '../dialogs/attributes.js';
|
||||
import noteRevisionsDialog from '../dialogs/note_revisions.js';
|
||||
import noteSourceDialog from '../dialogs/note_source.js';
|
||||
|
@ -12,8 +12,6 @@ import recentChangesDialog from "../dialogs/recent_changes.js";
|
||||
import sqlConsoleDialog from "../dialogs/sql_console.js";
|
||||
import searchNotesService from "./search_notes.js";
|
||||
import attributesDialog from "../dialogs/attributes.js";
|
||||
import labelsDialog from "../dialogs/labels.js";
|
||||
import relationsDialog from "../dialogs/relations.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
|
||||
function registerEntrypoints() {
|
||||
@ -42,12 +40,6 @@ function registerEntrypoints() {
|
||||
$(".show-attributes-button").click(attributesDialog.showDialog);
|
||||
utils.bindShortcut('alt+a', attributesDialog.showDialog);
|
||||
|
||||
$(".show-labels-button").click(labelsDialog.showDialog);
|
||||
utils.bindShortcut('alt+l', labelsDialog.showDialog);
|
||||
|
||||
$(".show-relations-button").click(relationsDialog.showDialog);
|
||||
utils.bindShortcut('alt+r', relationsDialog.showDialog);
|
||||
|
||||
$("#options-button").click(optionsDialog.showDialog);
|
||||
|
||||
utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
|
||||
|
@ -29,10 +29,6 @@ const $noteDetailComponentWrapper = $("#note-detail-component-wrapper");
|
||||
const $noteIdDisplay = $("#note-id-display");
|
||||
const $attributeList = $("#attribute-list");
|
||||
const $attributeListInner = $("#attribute-list-inner");
|
||||
const $labelList = $("#label-list");
|
||||
const $labelListInner = $("#label-list-inner");
|
||||
const $relationList = $("#relation-list");
|
||||
const $relationListInner = $("#relation-list-inner");
|
||||
const $childrenOverview = $("#children-overview");
|
||||
const $scriptArea = $("#note-detail-script-area");
|
||||
const $promotedAttributesContainer = $("#note-detail-promoted-attributes");
|
||||
@ -187,18 +183,14 @@ async function loadNoteDetail(noteId) {
|
||||
// after loading new note make sure editor is scrolled to the top
|
||||
$noteDetailWrapper.scrollTop(0);
|
||||
|
||||
const labels = await loadLabelList();
|
||||
|
||||
const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
|
||||
await showChildrenOverview(hideChildrenOverview);
|
||||
|
||||
await loadRelationList();
|
||||
|
||||
$scriptArea.html('');
|
||||
|
||||
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
|
||||
|
||||
await loadAttributes();
|
||||
const attributes = await loadAttributes();
|
||||
|
||||
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview');
|
||||
await showChildrenOverview(hideChildrenOverview);
|
||||
}
|
||||
|
||||
async function showChildrenOverview(hideChildrenOverview) {
|
||||
@ -411,50 +403,8 @@ async function loadAttributes() {
|
||||
$attributeList.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLabelList() {
|
||||
const noteId = getCurrentNoteId();
|
||||
|
||||
const labels = await server.get('notes/' + noteId + '/labels');
|
||||
|
||||
$labelListInner.html('');
|
||||
|
||||
if (labels.length > 0) {
|
||||
for (const label of labels) {
|
||||
$labelListInner.append(utils.formatLabel(label) + " ");
|
||||
}
|
||||
|
||||
$labelList.show();
|
||||
}
|
||||
else {
|
||||
$labelList.hide();
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
async function loadRelationList() {
|
||||
const noteId = getCurrentNoteId();
|
||||
|
||||
const relations = await server.get('notes/' + noteId + '/relations');
|
||||
|
||||
$relationListInner.html('');
|
||||
|
||||
if (relations.length > 0) {
|
||||
for (const relation of relations) {
|
||||
$relationListInner.append(relation.name + " = ");
|
||||
$relationListInner.append(await linkService.createNoteLink(relation.targetNoteId));
|
||||
$relationListInner.append(" ");
|
||||
}
|
||||
|
||||
$relationList.show();
|
||||
}
|
||||
else {
|
||||
$relationList.hide();
|
||||
}
|
||||
|
||||
return relations;
|
||||
return attributes;
|
||||
}
|
||||
|
||||
async function loadNote(noteId) {
|
||||
@ -535,8 +485,6 @@ export default {
|
||||
newNoteCreated,
|
||||
focus,
|
||||
loadAttributes,
|
||||
loadLabelList,
|
||||
loadRelationList,
|
||||
saveNote,
|
||||
saveNoteIfChanged,
|
||||
noteChanged
|
||||
|
@ -6,56 +6,7 @@ const repository = require('../../services/repository');
|
||||
const Attribute = require('../../entities/attribute');
|
||||
|
||||
async function getEffectiveNoteAttributes(req) {
|
||||
const noteId = req.params.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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const attr of filteredAttributes) {
|
||||
attr.isOwned = attr.noteId === noteId;
|
||||
}
|
||||
|
||||
return filteredAttributes;
|
||||
return await attributeService.getEffectiveAttributes(req.params.noteId);
|
||||
}
|
||||
|
||||
async function updateNoteAttribute(req) {
|
||||
@ -136,7 +87,7 @@ async function updateNoteAttributes(req) {
|
||||
await attributeEntity.save();
|
||||
}
|
||||
|
||||
return await getEffectiveNoteAttributes(req);
|
||||
return await attributeService.getEffectiveAttributes(noteId);
|
||||
}
|
||||
|
||||
async function getAttributeNames(req) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
const labelService = require('../../services/labels');
|
||||
const scriptService = require('../../services/script');
|
||||
const relationService = require('../../services/relations');
|
||||
const attributeService = require('../../services/attributes');
|
||||
const repository = require('../../services/repository');
|
||||
|
||||
async function exec(req) {
|
||||
@ -40,9 +40,9 @@ async function getRelationBundles(req) {
|
||||
const noteId = req.params.noteId;
|
||||
const relationName = req.params.relationName;
|
||||
|
||||
const relations = await relationService.getEffectiveRelations(noteId);
|
||||
const filtered = relations.filter(relation => relation.name === relationName);
|
||||
const targetNoteIds = filtered.map(relation => relation.targetNoteId);
|
||||
const attributes = await attributeService.getEffectiveAttributes(noteId);
|
||||
const filtered = attributes.filter(attr => attr.type === 'relation' && attr.name === relationName);
|
||||
const targetNoteIds = filtered.map(relation => relation.value);
|
||||
const uniqueNoteIds = Array.from(new Set(targetNoteIds));
|
||||
|
||||
const bundles = [];
|
||||
|
@ -26,8 +26,6 @@ const anonymizationRoute = require('./api/anonymization');
|
||||
const cleanupRoute = require('./api/cleanup');
|
||||
const imageRoute = require('./api/image');
|
||||
const attributesRoute = require('./api/attributes');
|
||||
const labelsRoute = require('./api/labels');
|
||||
const relationsRoute = require('./api/relations');
|
||||
const scriptRoute = require('./api/script');
|
||||
const senderRoute = require('./api/sender');
|
||||
const filesRoute = require('./api/file_upload');
|
||||
@ -141,15 +139,6 @@ function register(app) {
|
||||
apiRoute(GET, '/api/attributes/names', attributesRoute.getAttributeNames);
|
||||
apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute);
|
||||
|
||||
apiRoute(GET, '/api/notes/:noteId/labels', labelsRoute.getNoteLabels);
|
||||
apiRoute(PUT, '/api/notes/:noteId/labels', labelsRoute.updateNoteLabels);
|
||||
apiRoute(GET, '/api/labels/names', labelsRoute.getAllLabelNames);
|
||||
apiRoute(GET, '/api/labels/values/:labelName', labelsRoute.getValuesForLabel);
|
||||
|
||||
apiRoute(GET, '/api/notes/:noteId/relations', relationsRoute.getNoteRelations);
|
||||
apiRoute(PUT, '/api/notes/:noteId/relations', relationsRoute.updateNoteRelations);
|
||||
apiRoute(GET, '/api/relations/names', relationsRoute.getAllRelationNames);
|
||||
|
||||
route(GET, '/api/images/:imageId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage);
|
||||
route(POST, '/api/images', [auth.checkApiAuthOrElectron, uploadMiddleware], imageRoute.uploadImage, apiResultHandler);
|
||||
|
||||
|
@ -22,7 +22,7 @@ const BUILTIN_ATTRIBUTES = [
|
||||
{ type: 'relation', name: 'runOnNoteTitleChange' }
|
||||
];
|
||||
|
||||
async function getNotesWithAttribute(name, value) {
|
||||
async function getNotesWithLabel(name, value) {
|
||||
let notes;
|
||||
|
||||
if (value !== undefined) {
|
||||
@ -37,8 +37,8 @@ async function getNotesWithAttribute(name, value) {
|
||||
return notes;
|
||||
}
|
||||
|
||||
async function getNoteWithAttribute(name, value) {
|
||||
const notes = await getNotesWithAttribute(name, value);
|
||||
async function getNoteWithLabel(name, value) {
|
||||
const notes = await getNotesWithLabel(name, value);
|
||||
|
||||
return notes.length > 0 ? notes[0] : null;
|
||||
}
|
||||
@ -70,10 +70,62 @@ async function getAttributeNames(type, nameLike) {
|
||||
return names;
|
||||
}
|
||||
|
||||
async function getEffectiveAttributes(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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const attr of filteredAttributes) {
|
||||
attr.isOwned = attr.noteId === noteId;
|
||||
}
|
||||
|
||||
return filteredAttributes;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNotesWithAttribute,
|
||||
getNoteWithAttribute,
|
||||
getNotesWithLabel,
|
||||
getNoteWithLabel,
|
||||
createAttribute,
|
||||
getAttributeNames,
|
||||
getEffectiveAttributes,
|
||||
BUILTIN_ATTRIBUTES
|
||||
};
|
@ -3,7 +3,7 @@ const noteService = require('./notes');
|
||||
const sql = require('./sql');
|
||||
const utils = require('./utils');
|
||||
const dateUtils = require('./date_utils');
|
||||
const labelService = require('./labels');
|
||||
const attributeService = require('./attributes');
|
||||
const dateNoteService = require('./date_notes');
|
||||
const treeService = require('./tree');
|
||||
const config = require('./config');
|
||||
@ -45,15 +45,14 @@ function ScriptApi(startNote, currentNote, workNote) {
|
||||
|
||||
this.getNote = repository.getNote;
|
||||
this.getBranch = repository.getBranch;
|
||||
this.getLabel = repository.getLabel;
|
||||
this.getRelation = repository.getRelation;
|
||||
this.getAttribute = repository.getAttribute;
|
||||
this.getImage = repository.getImage;
|
||||
this.getEntity = repository.getEntity;
|
||||
this.getEntities = repository.getEntities;
|
||||
|
||||
this.createLabel = labelService.createLabel;
|
||||
this.getNotesWithLabel = labelService.getNotesWithLabel;
|
||||
this.getNoteWithLabel = labelService.getNoteWithLabel;
|
||||
this.createAttribute = attributeService.createAttribute;
|
||||
this.getNotesWithLabel = attributeService.getNotesWithLabel;
|
||||
this.getNoteWithLabel = attributeService.getNoteWithLabel;
|
||||
|
||||
this.createNote = noteService.createNote;
|
||||
|
||||
|
@ -168,10 +168,8 @@
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li><a id="show-note-revisions-button">Note revisions</a></li>
|
||||
<li><a id="show-note-revisions-button">Revisions</a></li>
|
||||
<li><a class="show-attributes-button"><kbd>Alt+A</kbd> Attributes</a></li>
|
||||
<li><a class="show-labels-button"><kbd>Alt+L</kbd> Labels</a></li>
|
||||
<li><a class="show-relations-button"><kbd>Alt+R</kbd> Relations</a></li>
|
||||
<li><a id="show-source-button">HTML source</a></li>
|
||||
<li><a id="upload-file-button">Upload file</a></li>
|
||||
</ul>
|
||||
@ -264,20 +262,6 @@
|
||||
|
||||
<span id="attribute-list-inner"></span>
|
||||
</div>
|
||||
|
||||
<div id="labels-and-relations" style="display: none;">
|
||||
<span id="label-list">
|
||||
<button class="btn btn-sm show-labels-button">Labels:</button>
|
||||
|
||||
<span id="label-list-inner"></span>
|
||||
</span>
|
||||
|
||||
<span id="relation-list">
|
||||
<button class="btn btn-sm show-relations-button">Relations:</button>
|
||||
|
||||
<span id="relation-list-inner"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -667,105 +651,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="labels-dialog" title="Note labels" style="display: none; padding: 20px;">
|
||||
<form data-bind="submit: save">
|
||||
<div style="text-align: center">
|
||||
<button class="btn btn-large" style="width: 200px;" id="save-labels-button" type="submit">Save changes <kbd>enter</kbd></button>
|
||||
</div>
|
||||
|
||||
<div style="height: 97%; overflow: auto">
|
||||
<table id="labels-table" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: labels">
|
||||
<tr data-bind="if: !isDeleted">
|
||||
<td class="handle">
|
||||
<span class="glyphicon glyphicon-resize-vertical"></span>
|
||||
<input type="hidden" name="position" data-bind="value: position"/>
|
||||
</td>
|
||||
<!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
|
||||
<td data-bind="text: labelId" style="width: 150px;"></td>
|
||||
<td>
|
||||
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
||||
<input type="text" class="label-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.labelChanged }"/>
|
||||
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate label.</div>
|
||||
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Label name can't be empty.</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="label-value form-control" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.labelChanged }" style="width: 300px"/>
|
||||
</td>
|
||||
<td title="Delete" style="padding: 13px; cursor: pointer;">
|
||||
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteLabel"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="relations-dialog" title="Note relations" style="display: none; padding: 20px;">
|
||||
<form data-bind="submit: save">
|
||||
<div style="text-align: center">
|
||||
<button class="btn btn-large" style="width: 200px;" id="save-relations-button" type="submit">Save changes <kbd>enter</kbd></button>
|
||||
</div>
|
||||
|
||||
<div style="height: 97%; overflow: auto">
|
||||
<table id="relations-table" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>ID</th>
|
||||
<th>Relation name</th>
|
||||
<th>Target note</th>
|
||||
<th>Inheritable</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: relations">
|
||||
<tr data-bind="if: !isDeleted">
|
||||
<td class="handle">
|
||||
<span class="glyphicon glyphicon-resize-vertical"></span>
|
||||
<input type="hidden" name="position" data-bind="value: position"/>
|
||||
</td>
|
||||
<!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
|
||||
<td data-bind="text: relationId" style="width: 150px;"></td>
|
||||
<td>
|
||||
<!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
|
||||
<input type="text" class="relation-name form-control" data-bind="value: name, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"/>
|
||||
<div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate relation.</div>
|
||||
<div style="color: red" data-bind="if: $parent.isEmptyName($index())">Relation name can't be empty.</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input class="form-control relation-target-note-id"
|
||||
placeholder="search for note by its name"
|
||||
data-bind="value: targetNoteId, valueUpdate: 'blur', event: { blur: $parent.relationChanged }"
|
||||
style="width: 300px;">
|
||||
|
||||
<span class="input-group-addon relations-show-recent-notes" title="Show recent notes" style="background: url('/images/icons/clock-16.png') no-repeat center; cursor: pointer;"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td title="Inheritable relations are automatically inherited to the child notes">
|
||||
<input type="checkbox" value="1" data-bind="checked: isInheritable" />
|
||||
</td>
|
||||
<td title="Delete" style="padding: 13px; cursor: pointer;">
|
||||
<span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteRelation"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="tooltip" style="display: none;"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
Loading…
x
Reference in New Issue
Block a user