2018-08-03 17:11:57 +08:00
|
|
|
import noteDetailService from '../services/note_detail.js';
|
|
|
|
import server from '../services/server.js';
|
|
|
|
import infoService from "../services/info.js";
|
2018-08-06 17:30:37 +08:00
|
|
|
import treeUtils from "../services/tree_utils.js";
|
2018-11-09 03:01:25 +08:00
|
|
|
import attributeService from "../services/attributes.js";
|
2018-11-13 17:25:59 +08:00
|
|
|
import attributeAutocompleteService from "../services/attribute_autocomplete.js";
|
2018-08-03 17:11:57 +08:00
|
|
|
|
|
|
|
const $dialog = $("#attributes-dialog");
|
|
|
|
const $saveAttributesButton = $("#save-attributes-button");
|
2018-08-07 17:38:00 +08:00
|
|
|
const $ownedAttributesBody = $('#owned-attributes-table tbody');
|
2018-08-03 17:11:57 +08:00
|
|
|
|
|
|
|
const attributesModel = new AttributesModel();
|
|
|
|
|
|
|
|
function AttributesModel() {
|
|
|
|
const self = this;
|
|
|
|
|
2018-08-07 17:38:00 +08:00
|
|
|
this.ownedAttributes = ko.observableArray();
|
|
|
|
this.inheritedAttributes = ko.observableArray();
|
2018-08-03 17:11:57 +08:00
|
|
|
|
|
|
|
this.availableTypes = [
|
|
|
|
{ text: "Label", value: "label" },
|
2018-08-06 02:08:56 +08:00
|
|
|
{ text: "Label definition", value: "label-definition" },
|
|
|
|
{ text: "Relation", value: "relation" },
|
|
|
|
{ text: "Relation definition", value: "relation-definition" }
|
2018-08-03 17:11:57 +08:00
|
|
|
];
|
|
|
|
|
2018-08-06 02:08:56 +08:00
|
|
|
this.availableLabelTypes = [
|
2018-08-04 04:56:49 +08:00
|
|
|
{ text: "Text", value: "text" },
|
2018-08-06 14:59:26 +08:00
|
|
|
{ text: "Number", value: "number" },
|
2018-08-04 04:56:49 +08:00
|
|
|
{ text: "Boolean", value: "boolean" },
|
2018-08-18 21:00:52 +08:00
|
|
|
{ text: "Date", value: "date" },
|
|
|
|
{ text: "URL", value: "url"}
|
2018-08-04 04:56:49 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
this.multiplicityTypes = [
|
|
|
|
{ text: "Single value", value: "singlevalue" },
|
|
|
|
{ text: "Multi value", value: "multivalue" }
|
|
|
|
];
|
|
|
|
|
2018-08-03 19:06:56 +08:00
|
|
|
this.typeChanged = function(data, event) {
|
|
|
|
self.getTargetAttribute(event.target).valueHasMutated();
|
|
|
|
};
|
|
|
|
|
2018-08-06 02:08:56 +08:00
|
|
|
this.labelTypeChanged = function(data, event) {
|
2018-08-04 04:56:49 +08:00
|
|
|
self.getTargetAttribute(event.target).valueHasMutated();
|
|
|
|
};
|
|
|
|
|
2018-08-03 17:11:57 +08:00
|
|
|
this.updateAttributePositions = function() {
|
|
|
|
let position = 0;
|
|
|
|
|
|
|
|
// we need to update positions by searching in the DOM, because order of the
|
2018-08-07 17:38:00 +08:00
|
|
|
// attributes in the viewmodel (self.ownedAttributes()) stays the same
|
|
|
|
$ownedAttributesBody.find('input[name="position"]').each(function() {
|
2018-08-03 17:11:57 +08:00
|
|
|
const attribute = self.getTargetAttribute(this);
|
|
|
|
|
|
|
|
attribute().position = position++;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2018-08-06 17:30:37 +08:00
|
|
|
async function showAttributes(attributes) {
|
2018-08-07 17:38:00 +08:00
|
|
|
const ownedAttributes = attributes.filter(attr => attr.isOwned);
|
|
|
|
|
|
|
|
for (const attr of ownedAttributes) {
|
2018-08-03 19:06:56 +08:00
|
|
|
attr.labelValue = attr.type === 'label' ? attr.value : '';
|
2018-11-08 03:32:11 +08:00
|
|
|
attr.relationValue = attr.type === 'relation' ? (await treeUtils.getNoteTitle(attr.value)) : '';
|
|
|
|
attr.selectedPath = attr.type === 'relation' ? attr.value : '';
|
2018-08-06 14:59:26 +08:00
|
|
|
attr.labelDefinition = (attr.type === 'label-definition' && attr.value) ? attr.value : {
|
2018-08-06 02:08:56 +08:00
|
|
|
labelType: "text",
|
2018-08-04 04:56:49 +08:00
|
|
|
multiplicityType: "singlevalue",
|
2018-12-01 00:36:41 +08:00
|
|
|
isPromoted: true,
|
|
|
|
numberPrecision: 0
|
2018-08-06 02:08:56 +08:00
|
|
|
};
|
2018-08-20 16:04:26 +08:00
|
|
|
|
|
|
|
attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : {
|
2018-08-06 02:08:56 +08:00
|
|
|
multiplicityType: "singlevalue",
|
2018-11-19 19:07:33 +08:00
|
|
|
inverseRelation: "",
|
2018-08-06 02:08:56 +08:00
|
|
|
isPromoted: true
|
2018-08-04 04:56:49 +08:00
|
|
|
};
|
2018-08-06 14:59:26 +08:00
|
|
|
|
|
|
|
delete attr.value;
|
2018-08-03 19:06:56 +08:00
|
|
|
}
|
|
|
|
|
2018-08-07 17:38:00 +08:00
|
|
|
self.ownedAttributes(ownedAttributes.map(ko.observable));
|
2018-08-03 17:11:57 +08:00
|
|
|
|
|
|
|
addLastEmptyRow();
|
2018-08-07 17:38:00 +08:00
|
|
|
|
|
|
|
const inheritedAttributes = attributes.filter(attr => !attr.isOwned);
|
|
|
|
|
|
|
|
self.inheritedAttributes(inheritedAttributes);
|
2018-08-06 02:08:56 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
this.loadAttributes = async function() {
|
|
|
|
const noteId = noteDetailService.getCurrentNoteId();
|
|
|
|
|
|
|
|
const attributes = await server.get('notes/' + noteId + '/attributes');
|
|
|
|
|
2018-08-06 17:30:37 +08:00
|
|
|
await showAttributes(attributes);
|
2018-08-03 17:11:57 +08:00
|
|
|
|
|
|
|
// attribute might not be rendered immediatelly so could not focus
|
2018-08-18 20:49:25 +08:00
|
|
|
setTimeout(() => $(".attribute-type-select:last").focus(), 100);
|
2018-08-03 17:11:57 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
this.deleteAttribute = function(data, event) {
|
|
|
|
const attribute = self.getTargetAttribute(event.target);
|
|
|
|
const attributeData = attribute();
|
|
|
|
|
|
|
|
if (attributeData) {
|
2018-08-07 17:38:00 +08:00
|
|
|
attributeData.isDeleted = true;
|
2018-08-03 17:11:57 +08:00
|
|
|
|
|
|
|
attribute(attributeData);
|
|
|
|
|
|
|
|
addLastEmptyRow();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function isValid() {
|
2018-08-07 17:38:00 +08:00
|
|
|
for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) {
|
2018-11-20 04:58:52 +08:00
|
|
|
if (self.isEmptyName(i) || self.isEmptyRelationTarget(i)) {
|
2018-08-03 17:11:57 +08:00
|
|
|
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();
|
|
|
|
|
2018-08-07 17:38:00 +08:00
|
|
|
const attributesToSave = self.ownedAttributes()
|
2018-08-03 17:11:57 +08:00
|
|
|
.map(attribute => attribute())
|
|
|
|
.filter(attribute => attribute.attributeId !== "" || attribute.name !== "");
|
|
|
|
|
2018-08-06 02:08:56 +08:00
|
|
|
for (const attr of attributesToSave) {
|
|
|
|
if (attr.type === 'label') {
|
|
|
|
attr.value = attr.labelValue;
|
|
|
|
}
|
|
|
|
else if (attr.type === 'relation') {
|
2018-11-08 03:32:11 +08:00
|
|
|
attr.value = treeUtils.getNoteIdFromNotePath(attr.selectedPath);
|
2018-08-06 02:08:56 +08:00
|
|
|
}
|
|
|
|
else if (attr.type === 'label-definition') {
|
2018-08-06 14:59:26 +08:00
|
|
|
attr.value = attr.labelDefinition;
|
2018-08-06 02:08:56 +08:00
|
|
|
}
|
|
|
|
else if (attr.type === 'relation-definition') {
|
2018-08-06 14:59:26 +08:00
|
|
|
attr.value = attr.relationDefinition;
|
2018-08-06 02:08:56 +08:00
|
|
|
}
|
2018-08-03 17:11:57 +08:00
|
|
|
|
2018-08-06 02:08:56 +08:00
|
|
|
delete attr.labelValue;
|
|
|
|
delete attr.relationValue;
|
|
|
|
delete attr.labelDefinition;
|
|
|
|
delete attr.relationDefinition;
|
|
|
|
}
|
2018-08-03 17:11:57 +08:00
|
|
|
|
2018-08-06 02:08:56 +08:00
|
|
|
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
|
|
|
|
|
2018-08-06 17:30:37 +08:00
|
|
|
await showAttributes(attributes);
|
2018-08-03 17:11:57 +08:00
|
|
|
|
|
|
|
infoService.showMessage("Attributes have been saved.");
|
|
|
|
|
2018-11-09 03:01:25 +08:00
|
|
|
attributeService.refreshAttributes();
|
2018-08-03 17:11:57 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
function addLastEmptyRow() {
|
2018-08-07 17:38:00 +08:00
|
|
|
const attributes = self.ownedAttributes().filter(attr => !attr().isDeleted);
|
2018-08-03 17:11:57 +08:00
|
|
|
const last = attributes.length === 0 ? null : attributes[attributes.length - 1]();
|
|
|
|
|
2018-08-03 19:06:56 +08:00
|
|
|
if (!last || last.name.trim() !== "") {
|
2018-08-07 17:38:00 +08:00
|
|
|
self.ownedAttributes.push(ko.observable({
|
2018-08-03 17:11:57 +08:00
|
|
|
attributeId: '',
|
|
|
|
type: 'label',
|
|
|
|
name: '',
|
2018-08-03 19:06:56 +08:00
|
|
|
labelValue: '',
|
|
|
|
relationValue: '',
|
|
|
|
isInheritable: false,
|
2018-08-07 17:38:00 +08:00
|
|
|
isDeleted: false,
|
2018-08-04 04:56:49 +08:00
|
|
|
position: 0,
|
2018-08-06 02:08:56 +08:00
|
|
|
labelDefinition: {
|
2018-08-06 02:48:56 +08:00
|
|
|
labelType: "text",
|
|
|
|
multiplicityType: "singlevalue",
|
2018-12-01 00:36:41 +08:00
|
|
|
isPromoted: true,
|
|
|
|
numberPrecision: 0
|
2018-08-06 02:48:56 +08:00
|
|
|
},
|
|
|
|
relationDefinition: {
|
2018-08-04 04:56:49 +08:00
|
|
|
multiplicityType: "singlevalue",
|
2018-11-19 19:07:33 +08:00
|
|
|
inverseRelation: "",
|
2018-08-06 02:08:56 +08:00
|
|
|
isPromoted: true
|
2018-08-04 04:56:49 +08:00
|
|
|
}
|
2018-08-03 17:11:57 +08:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.attributeChanged = function (data, event) {
|
|
|
|
addLastEmptyRow();
|
|
|
|
|
|
|
|
const attribute = self.getTargetAttribute(event.target);
|
|
|
|
|
|
|
|
attribute.valueHasMutated();
|
|
|
|
};
|
|
|
|
|
|
|
|
this.isEmptyName = function(index) {
|
2018-08-07 17:38:00 +08:00
|
|
|
const cur = self.ownedAttributes()[index]();
|
2018-08-03 17:11:57 +08:00
|
|
|
|
2018-11-20 05:06:51 +08:00
|
|
|
if (cur.name.trim() || cur.isDeleted) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cur.attributeId) {
|
|
|
|
// name is empty and attribute already exists so this is NO-GO
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cur.type === 'relation-definition' || cur.type === 'label-definition') {
|
|
|
|
// for definitions there's no possible empty value so we always require name
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cur.type === 'label' && cur.labelValue) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cur.type === 'relation' && cur.relationValue) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2018-08-03 17:11:57 +08:00
|
|
|
};
|
|
|
|
|
2018-11-20 04:58:52 +08:00
|
|
|
this.isEmptyRelationTarget = function(index) {
|
|
|
|
const cur = self.ownedAttributes()[index]();
|
|
|
|
|
|
|
|
return cur.type === "relation" && !cur.isDeleted && cur.name && !cur.relationValue;
|
|
|
|
};
|
|
|
|
|
2018-08-03 17:11:57 +08:00
|
|
|
this.getTargetAttribute = function(target) {
|
|
|
|
const context = ko.contextFor(target);
|
|
|
|
const index = context.$index();
|
|
|
|
|
2018-08-07 17:38:00 +08:00
|
|
|
return self.ownedAttributes()[index];
|
2018-08-03 17:11:57 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function showDialog() {
|
2018-11-07 00:47:40 +08:00
|
|
|
// lazily apply bindings on first use
|
|
|
|
if (!ko.dataFor($dialog[0])) {
|
|
|
|
ko.applyBindings(attributesModel, $dialog[0]);
|
|
|
|
}
|
|
|
|
|
2018-08-03 17:11:57 +08:00
|
|
|
glob.activeDialog = $dialog;
|
|
|
|
|
|
|
|
await attributesModel.loadAttributes();
|
|
|
|
|
2018-11-06 22:53:23 +08:00
|
|
|
$dialog.modal();
|
2018-08-03 17:11:57 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
$dialog.on('focus', '.attribute-name', function (e) {
|
2018-11-13 17:25:59 +08:00
|
|
|
attributeAutocompleteService.initAttributeNameAutocomplete({
|
|
|
|
$el: $(this),
|
2018-11-13 17:42:55 +08:00
|
|
|
attributeType: () => {
|
2018-11-13 17:25:59 +08:00
|
|
|
const attribute = attributesModel.getTargetAttribute(this);
|
|
|
|
return (attribute().type === 'relation' || attribute().type === 'relation-definition') ? 'relation' : 'label';
|
|
|
|
},
|
|
|
|
open: true
|
|
|
|
});
|
2018-08-03 17:11:57 +08:00
|
|
|
});
|
|
|
|
|
2018-11-13 17:25:59 +08:00
|
|
|
$dialog.on('focus', '.label-value', function (e) {
|
|
|
|
attributeAutocompleteService.initLabelValueAutocomplete({
|
|
|
|
$el: $(this),
|
|
|
|
open: true
|
|
|
|
})
|
2018-08-03 17:11:57 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
export default {
|
|
|
|
showDialog
|
|
|
|
};
|