trilium/src/public/javascripts/dialogs/attributes.js

353 lines
11 KiB
JavaScript
Raw Normal View History

import noteDetailService from '../services/note_detail.js';
import server from '../services/server.js';
import infoService from "../services/info.js";
const $dialog = $("#attributes-dialog");
const $saveAttributesButton = $("#save-attributes-button");
const $attributesBody = $('#attributes-table tbody');
const attributesModel = new AttributesModel();
function AttributesModel() {
const self = this;
this.attributes = ko.observableArray();
this.availableTypes = [
{ text: "Label", value: "label" },
{ text: "Label definition", value: "label-definition" },
{ text: "Relation", value: "relation" },
{ text: "Relation definition", value: "relation-definition" }
];
this.availableLabelTypes = [
2018-08-04 04:56:49 +08:00
{ text: "Text", value: "text" },
{ text: "Number", value: "number" },
2018-08-04 04:56:49 +08:00
{ text: "Boolean", value: "boolean" },
{ text: "Date", value: "date" },
{ text: "DateTime", value: "datetime" }
2018-08-04 04:56:49 +08:00
];
this.multiplicityTypes = [
{ text: "Single value", value: "singlevalue" },
{ text: "Multi value", value: "multivalue" }
];
this.typeChanged = function(data, event) {
self.getTargetAttribute(event.target).valueHasMutated();
};
this.labelTypeChanged = function(data, event) {
2018-08-04 04:56:49 +08:00
self.getTargetAttribute(event.target).valueHasMutated();
};
this.updateAttributePositions = function() {
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() {
const attribute = self.getTargetAttribute(this);
attribute().position = position++;
});
};
function prepareAttributes(attributes) {
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) ? attr.value : {
labelType: "text",
2018-08-04 04:56:49 +08:00
multiplicityType: "singlevalue",
isPromoted: true
};
attr.relationDefinition = attr.type === ('relation-definition' && attr.value) ? attr.value : {
multiplicityType: "singlevalue",
isPromoted: true
2018-08-04 04:56:49 +08:00
};
delete attr.value;
}
self.attributes(attributes.map(ko.observable));
addLastEmptyRow();
}
this.loadAttributes = async function() {
const noteId = noteDetailService.getCurrentNoteId();
const attributes = await server.get('notes/' + noteId + '/attributes');
prepareAttributes(attributes);
// attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-name:last").focus(), 100);
$attributesBody.sortable({
handle: '.handle',
containment: $attributesBody,
update: this.updateAttributePositions
});
};
this.deleteAttribute = function(data, event) {
const attribute = self.getTargetAttribute(event.target);
const attributeData = attribute();
if (attributeData) {
attributeData.isDeleted = 1;
attribute(attributeData);
addLastEmptyRow();
}
};
function isValid() {
for (let attributes = self.attributes(), i = 0; i < attributes.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.
$saveAttributesButton.focus();
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
return;
}
self.updateAttributePositions();
const noteId = noteDetailService.getCurrentNoteId();
const attributesToSave = self.attributes()
.map(attribute => attribute())
.filter(attribute => attribute.attributeId !== "" || attribute.name !== "");
for (const attr of attributesToSave) {
if (attr.type === 'label') {
attr.value = attr.labelValue;
}
else if (attr.type === 'relation') {
attr.value = attr.relationValue;
}
else if (attr.type === 'label-definition') {
attr.value = attr.labelDefinition;
}
else if (attr.type === 'relation-definition') {
attr.value = attr.relationDefinition;
}
delete attr.labelValue;
delete attr.relationValue;
delete attr.labelDefinition;
delete attr.relationDefinition;
}
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
prepareAttributes(attributes);
infoService.showMessage("Attributes have been saved.");
noteDetailService.loadAttributes();
};
function addLastEmptyRow() {
const attributes = self.attributes().filter(attr => attr().isDeleted === 0);
const last = attributes.length === 0 ? null : attributes[attributes.length - 1]();
if (!last || last.name.trim() !== "") {
self.attributes.push(ko.observable({
attributeId: '',
type: 'label',
name: '',
labelValue: '',
relationValue: '',
isInheritable: false,
isDeleted: 0,
2018-08-04 04:56:49 +08:00
position: 0,
labelDefinition: {
2018-08-06 02:48:56 +08:00
labelType: "text",
multiplicityType: "singlevalue",
isPromoted: true
},
relationDefinition: {
2018-08-04 04:56:49 +08:00
multiplicityType: "singlevalue",
isPromoted: true
2018-08-04 04:56:49 +08:00
}
}));
}
}
this.attributeChanged = function (data, event) {
addLastEmptyRow();
const attribute = self.getTargetAttribute(event.target);
attribute.valueHasMutated();
};
this.isNotUnique = function(index) {
const cur = self.attributes()[index]();
if (cur.name.trim() === "") {
return false;
}
for (let attributes = self.attributes(), i = 0; i < attributes.length; i++) {
const attribute = attributes[i]();
if (index !== i && cur.name === attribute.name && cur.type === attribute.type) {
return true;
}
}
return false;
};
this.isEmptyName = function(index) {
const cur = self.attributes()[index]();
return cur.name.trim() === "" && (cur.attributeId !== "" || cur.labelValue !== "" || cur.relationValue);
};
this.getTargetAttribute = function(target) {
const context = ko.contextFor(target);
const index = context.$index();
return self.attributes()[index];
}
}
async function showDialog() {
glob.activeDialog = $dialog;
await attributesModel.loadAttributes();
$dialog.dialog({
modal: true,
width: 950,
height: 500
});
}
ko.applyBindings(attributesModel, $dialog[0]);
$dialog.on('focus', '.attribute-name', function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
$(this).autocomplete({
source: async (request, response) => {
const attribute = attributesModel.getTargetAttribute(this);
2018-08-06 02:48:56 +08:00
const type = (attribute().type === 'relation' || attribute().type === 'relation-definition') ? 'relation' : 'label';
const names = await server.get('attributes/names/?type=' + type + '&query=' + encodeURIComponent(request.term));
const result = names.map(name => {
return {
label: name,
value: name
}
});
if (result.length > 0) {
response(result);
}
else {
response([{
label: "No results",
value: "No results"
}]);
}
},
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
$dialog.on('focus', '.label-value', async function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
const attributeName = $(this).parent().parent().find('.attribute-name').val();
if (attributeName.trim() === "") {
return;
}
const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
if (attributeValues.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: attributeValues.map(attribute => {
return {
attribute: attribute,
value: attribute
}
}),
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
};