mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
refactored attributes out of note detail, fixes #213
This commit is contained in:
parent
3ba761fe28
commit
6416e3e9fb
@ -2,7 +2,7 @@ import noteDetailService from '../services/note_detail.js';
|
|||||||
import server from '../services/server.js';
|
import server from '../services/server.js';
|
||||||
import infoService from "../services/info.js";
|
import infoService from "../services/info.js";
|
||||||
import treeUtils from "../services/tree_utils.js";
|
import treeUtils from "../services/tree_utils.js";
|
||||||
import linkService from "../services/link.js";
|
import attributeService from "../services/attributes.js";
|
||||||
|
|
||||||
const $dialog = $("#attributes-dialog");
|
const $dialog = $("#attributes-dialog");
|
||||||
const $saveAttributesButton = $("#save-attributes-button");
|
const $saveAttributesButton = $("#save-attributes-button");
|
||||||
@ -165,7 +165,7 @@ function AttributesModel() {
|
|||||||
|
|
||||||
infoService.showMessage("Attributes have been saved.");
|
infoService.showMessage("Attributes have been saved.");
|
||||||
|
|
||||||
noteDetailService.refreshAttributes();
|
attributeService.refreshAttributes();
|
||||||
};
|
};
|
||||||
|
|
||||||
function addLastEmptyRow() {
|
function addLastEmptyRow() {
|
||||||
|
298
src/public/javascripts/services/attributes.js
Normal file
298
src/public/javascripts/services/attributes.js
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import server from "./server.js";
|
||||||
|
import utils from "./utils.js";
|
||||||
|
import messagingService from "./messaging.js";
|
||||||
|
import treeUtils from "./tree_utils.js";
|
||||||
|
import noteAutocompleteService from "./note_autocomplete.js";
|
||||||
|
import treeService from "./tree.js";
|
||||||
|
import linkService from "./link.js";
|
||||||
|
import infoService from "./info.js";
|
||||||
|
import noteDetailService from "./note_detail.js";
|
||||||
|
|
||||||
|
const $attributeList = $("#attribute-list");
|
||||||
|
const $attributeListInner = $("#attribute-list-inner");
|
||||||
|
const $promotedAttributesContainer = $("#note-detail-promoted-attributes");
|
||||||
|
|
||||||
|
let attributePromise;
|
||||||
|
|
||||||
|
async function refreshAttributes() {
|
||||||
|
attributePromise = server.get('notes/' + noteDetailService.getCurrentNoteId() + '/attributes');
|
||||||
|
|
||||||
|
await showAttributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAttributes() {
|
||||||
|
return await attributePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showAttributes() {
|
||||||
|
$promotedAttributesContainer.empty();
|
||||||
|
$attributeList.hide();
|
||||||
|
|
||||||
|
const noteId = noteDetailService.getCurrentNoteId();
|
||||||
|
|
||||||
|
const attributes = await attributePromise;
|
||||||
|
|
||||||
|
const promoted = attributes.filter(attr =>
|
||||||
|
(attr.type === 'label-definition' || attr.type === 'relation-definition')
|
||||||
|
&& !attr.name.startsWith("child:")
|
||||||
|
&& attr.value.isPromoted);
|
||||||
|
|
||||||
|
let idx = 1;
|
||||||
|
|
||||||
|
async function createRow(definitionAttr, valueAttr) {
|
||||||
|
const definition = definitionAttr.value;
|
||||||
|
const inputId = "promoted-input-" + idx;
|
||||||
|
const $tr = $("<tr>");
|
||||||
|
const $labelCell = $("<th>").append(valueAttr.name);
|
||||||
|
const $input = $("<input>")
|
||||||
|
.prop("id", inputId)
|
||||||
|
.prop("tabindex", definitionAttr.position)
|
||||||
|
.prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
|
||||||
|
.prop("attribute-type", valueAttr.type)
|
||||||
|
.prop("attribute-name", valueAttr.name)
|
||||||
|
.prop("value", valueAttr.value)
|
||||||
|
.addClass("form-control")
|
||||||
|
.addClass("promoted-attribute-input")
|
||||||
|
.change(promotedAttributeChanged);
|
||||||
|
|
||||||
|
idx++;
|
||||||
|
|
||||||
|
const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
|
||||||
|
|
||||||
|
const $actionCell = $("<td>");
|
||||||
|
const $multiplicityCell = $("<td>");
|
||||||
|
|
||||||
|
$tr
|
||||||
|
.append($labelCell)
|
||||||
|
.append($inputCell)
|
||||||
|
.append($actionCell)
|
||||||
|
.append($multiplicityCell);
|
||||||
|
|
||||||
|
if (valueAttr.type === 'label') {
|
||||||
|
if (definition.labelType === 'text') {
|
||||||
|
$input.prop("type", "text");
|
||||||
|
|
||||||
|
// no need to await for this, can be done asynchronously
|
||||||
|
server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => {
|
||||||
|
if (attributeValues.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeValues = attributeValues.map(attribute => { return { value: attribute }; });
|
||||||
|
|
||||||
|
$input.autocomplete({
|
||||||
|
appendTo: document.querySelector('body'),
|
||||||
|
hint: false,
|
||||||
|
autoselect: true,
|
||||||
|
openOnFocus: true,
|
||||||
|
minLength: 0
|
||||||
|
}, [{
|
||||||
|
displayKey: 'value',
|
||||||
|
source: function (term, cb) {
|
||||||
|
term = term.toLowerCase();
|
||||||
|
|
||||||
|
const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term));
|
||||||
|
|
||||||
|
cb(filtered);
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (definition.labelType === 'number') {
|
||||||
|
$input.prop("type", "number");
|
||||||
|
}
|
||||||
|
else if (definition.labelType === 'boolean') {
|
||||||
|
$input.prop("type", "checkbox");
|
||||||
|
|
||||||
|
if (valueAttr.value === "true") {
|
||||||
|
$input.prop("checked", "checked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (definition.labelType === 'date') {
|
||||||
|
$input.prop("type", "date");
|
||||||
|
|
||||||
|
const $todayButton = $("<button>").addClass("btn btn-sm").text("Today").click(() => {
|
||||||
|
$input.val(utils.formatDateISO(new Date()));
|
||||||
|
$input.trigger("change");
|
||||||
|
});
|
||||||
|
|
||||||
|
$actionCell.append($todayButton);
|
||||||
|
}
|
||||||
|
else if (definition.labelType === 'url') {
|
||||||
|
$input.prop("placeholder", "http://website...");
|
||||||
|
|
||||||
|
const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => {
|
||||||
|
window.open($input.val(), '_blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
$actionCell.append($openButton);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
messagingService.logError("Unknown labelType=" + definitionAttr.labelType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (valueAttr.type === 'relation') {
|
||||||
|
if (valueAttr.value) {
|
||||||
|
$input.val(await treeUtils.getNoteTitle(valueAttr.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// no need to wait for this
|
||||||
|
noteAutocompleteService.initNoteAutocomplete($input);
|
||||||
|
|
||||||
|
$input.on('autocomplete:selected', function(event, suggestion, dataset) {
|
||||||
|
promotedAttributeChanged(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
$input.prop("data-selected-path", valueAttr.value);
|
||||||
|
|
||||||
|
// ideally we'd use link instead of button which would allow tooltip preview, but
|
||||||
|
// we can't guarantee updating the link in the a element
|
||||||
|
const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => {
|
||||||
|
const notePath = $input.prop("data-selected-path");
|
||||||
|
|
||||||
|
treeService.activateNote(notePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
$actionCell.append($openButton);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
messagingService.logError("Unknown attribute type=" + valueAttr.type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.multiplicityType === "multivalue") {
|
||||||
|
const addButton = $("<span>")
|
||||||
|
.addClass("glyphicon glyphicon-plus pointer")
|
||||||
|
.prop("title", "Add new attribute")
|
||||||
|
.click(async () => {
|
||||||
|
const $new = await createRow(definitionAttr, {
|
||||||
|
attributeId: "",
|
||||||
|
type: valueAttr.type,
|
||||||
|
name: definitionAttr.name,
|
||||||
|
value: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
$tr.after($new);
|
||||||
|
|
||||||
|
$new.find('input').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeButton = $("<span>")
|
||||||
|
.addClass("glyphicon glyphicon-trash pointer")
|
||||||
|
.prop("title", "Remove this attribute")
|
||||||
|
.click(async () => {
|
||||||
|
if (valueAttr.attributeId) {
|
||||||
|
await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tr.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
$multiplicityCell.append(addButton).append(" ").append(removeButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promoted.length > 0) {
|
||||||
|
const $tbody = $("<tbody>");
|
||||||
|
|
||||||
|
for (const definitionAttr of promoted) {
|
||||||
|
const definitionType = definitionAttr.type;
|
||||||
|
const valueType = definitionType.substr(0, definitionType.length - 11);
|
||||||
|
|
||||||
|
let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType);
|
||||||
|
|
||||||
|
if (valueAttrs.length === 0) {
|
||||||
|
valueAttrs.push({
|
||||||
|
attributeId: "",
|
||||||
|
type: valueType,
|
||||||
|
name: definitionAttr.name,
|
||||||
|
value: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definitionAttr.value.multiplicityType === 'singlevalue') {
|
||||||
|
valueAttrs = valueAttrs.slice(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const valueAttr of valueAttrs) {
|
||||||
|
const $tr = await createRow(definitionAttr, valueAttr);
|
||||||
|
|
||||||
|
$tbody.append($tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we replace the whole content in one step so there can't be any race conditions
|
||||||
|
// (previously we saw promoted attributes doubling)
|
||||||
|
$promotedAttributesContainer.empty().append($tbody);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$attributeListInner.empty();
|
||||||
|
|
||||||
|
if (attributes.length > 0) {
|
||||||
|
for (const attribute of attributes) {
|
||||||
|
if (attribute.type === 'label') {
|
||||||
|
$attributeListInner.append(utils.formatLabel(attribute) + " ");
|
||||||
|
}
|
||||||
|
else if (attribute.type === 'relation') {
|
||||||
|
if (attribute.value) {
|
||||||
|
$attributeListInner.append('@' + attribute.name + "=");
|
||||||
|
$attributeListInner.append(await linkService.createNoteLink(attribute.value));
|
||||||
|
$attributeListInner.append(" ");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
messagingService.logError(`Relation ${attribute.attributeId} has empty target`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') {
|
||||||
|
$attributeListInner.append(attribute.name + " definition ");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
messagingService.logError("Unknown attr type: " + attribute.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributeList.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promotedAttributeChanged(event) {
|
||||||
|
const $attr = $(event.target);
|
||||||
|
|
||||||
|
let value;
|
||||||
|
|
||||||
|
if ($attr.prop("type") === "checkbox") {
|
||||||
|
value = $attr.is(':checked') ? "true" : "false";
|
||||||
|
}
|
||||||
|
else if ($attr.prop("attribute-type") === "relation") {
|
||||||
|
const selectedPath = $attr.prop("data-selected-path");
|
||||||
|
|
||||||
|
if (selectedPath) {
|
||||||
|
value = treeUtils.getNoteIdFromNotePath(selectedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
value = $attr.val();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await server.put("notes/" + noteDetailService.getCurrentNoteId() + "/attribute", {
|
||||||
|
attributeId: $attr.prop("attribute-id"),
|
||||||
|
type: $attr.prop("attribute-type"),
|
||||||
|
name: $attr.prop("attribute-name"),
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
|
||||||
|
$attr.prop("attribute-id", result.attributeId);
|
||||||
|
|
||||||
|
infoService.showMessage("Attribute has been saved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getAttributes,
|
||||||
|
showAttributes,
|
||||||
|
refreshAttributes
|
||||||
|
}
|
@ -3,11 +3,9 @@ import treeUtils from './tree_utils.js';
|
|||||||
import noteTypeService from './note_type.js';
|
import noteTypeService from './note_type.js';
|
||||||
import protectedSessionService from './protected_session.js';
|
import protectedSessionService from './protected_session.js';
|
||||||
import protectedSessionHolder from './protected_session_holder.js';
|
import protectedSessionHolder from './protected_session_holder.js';
|
||||||
import utils from './utils.js';
|
|
||||||
import server from './server.js';
|
import server from './server.js';
|
||||||
import messagingService from "./messaging.js";
|
import messagingService from "./messaging.js";
|
||||||
import infoService from "./info.js";
|
import infoService from "./info.js";
|
||||||
import linkService from "./link.js";
|
|
||||||
import treeCache from "./tree_cache.js";
|
import treeCache from "./tree_cache.js";
|
||||||
import NoteFull from "../entities/note_full.js";
|
import NoteFull from "../entities/note_full.js";
|
||||||
import noteDetailCode from './note_detail_code.js';
|
import noteDetailCode from './note_detail_code.js';
|
||||||
@ -18,7 +16,7 @@ import noteDetailSearch from './note_detail_search.js';
|
|||||||
import noteDetailRender from './note_detail_render.js';
|
import noteDetailRender from './note_detail_render.js';
|
||||||
import noteDetailRelationMap from './note_detail_relation_map.js';
|
import noteDetailRelationMap from './note_detail_relation_map.js';
|
||||||
import bundleService from "./bundle.js";
|
import bundleService from "./bundle.js";
|
||||||
import noteAutocompleteService from "./note_autocomplete.js";
|
import attributeService from "./attributes.js";
|
||||||
|
|
||||||
const $noteTitle = $("#note-title");
|
const $noteTitle = $("#note-title");
|
||||||
|
|
||||||
@ -28,11 +26,8 @@ const $protectButton = $("#protect-button");
|
|||||||
const $unprotectButton = $("#unprotect-button");
|
const $unprotectButton = $("#unprotect-button");
|
||||||
const $noteDetailWrapper = $("#note-detail-wrapper");
|
const $noteDetailWrapper = $("#note-detail-wrapper");
|
||||||
const $noteIdDisplay = $("#note-id-display");
|
const $noteIdDisplay = $("#note-id-display");
|
||||||
const $attributeList = $("#attribute-list");
|
|
||||||
const $attributeListInner = $("#attribute-list-inner");
|
|
||||||
const $childrenOverview = $("#children-overview");
|
const $childrenOverview = $("#children-overview");
|
||||||
const $scriptArea = $("#note-detail-script-area");
|
const $scriptArea = $("#note-detail-script-area");
|
||||||
const $promotedAttributesContainer = $("#note-detail-promoted-attributes");
|
|
||||||
|
|
||||||
let currentNote = null;
|
let currentNote = null;
|
||||||
|
|
||||||
@ -40,8 +35,6 @@ let noteChangeDisabled = false;
|
|||||||
|
|
||||||
let isNoteChanged = false;
|
let isNoteChanged = false;
|
||||||
|
|
||||||
let attributePromise;
|
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
'code': noteDetailCode,
|
'code': noteDetailCode,
|
||||||
'text': noteDetailText,
|
'text': noteDetailText,
|
||||||
@ -181,7 +174,7 @@ async function loadNoteDetail(noteId) {
|
|||||||
currentNote = loadedNote;
|
currentNote = loadedNote;
|
||||||
|
|
||||||
// needs to happend after loading the note itself because it references current noteId
|
// needs to happend after loading the note itself because it references current noteId
|
||||||
refreshAttributes();
|
attributeService.refreshAttributes();
|
||||||
|
|
||||||
if (isNewNoteCreated) {
|
if (isNewNoteCreated) {
|
||||||
isNewNoteCreated = false;
|
isNewNoteCreated = false;
|
||||||
@ -232,14 +225,14 @@ async function loadNoteDetail(noteId) {
|
|||||||
|
|
||||||
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
|
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
|
||||||
|
|
||||||
await showAttributes();
|
await attributeService.showAttributes();
|
||||||
|
|
||||||
await showChildrenOverview();
|
await showChildrenOverview();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showChildrenOverview() {
|
async function showChildrenOverview() {
|
||||||
const note = getCurrentNote();
|
const note = getCurrentNote();
|
||||||
const attributes = await attributePromise;
|
const attributes = await attributeService.getAttributes();
|
||||||
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview')
|
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview')
|
||||||
|| note.type === 'relation-map'
|
|| note.type === 'relation-map'
|
||||||
|| note.type === 'image'
|
|| note.type === 'image'
|
||||||
@ -267,283 +260,6 @@ async function showChildrenOverview() {
|
|||||||
$childrenOverview.show();
|
$childrenOverview.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAttributes() {
|
|
||||||
attributePromise = server.get('notes/' + getCurrentNoteId() + '/attributes');
|
|
||||||
|
|
||||||
await showAttributes();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAttributes() {
|
|
||||||
return await attributePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function showAttributes() {
|
|
||||||
$promotedAttributesContainer.empty();
|
|
||||||
$attributeList.hide();
|
|
||||||
|
|
||||||
const noteId = getCurrentNoteId();
|
|
||||||
|
|
||||||
const attributes = await attributePromise;
|
|
||||||
|
|
||||||
const promoted = attributes.filter(attr =>
|
|
||||||
(attr.type === 'label-definition' || attr.type === 'relation-definition')
|
|
||||||
&& !attr.name.startsWith("child:")
|
|
||||||
&& attr.value.isPromoted);
|
|
||||||
|
|
||||||
let idx = 1;
|
|
||||||
|
|
||||||
async function createRow(definitionAttr, valueAttr) {
|
|
||||||
const definition = definitionAttr.value;
|
|
||||||
const inputId = "promoted-input-" + idx;
|
|
||||||
const $tr = $("<tr>");
|
|
||||||
const $labelCell = $("<th>").append(valueAttr.name);
|
|
||||||
const $input = $("<input>")
|
|
||||||
.prop("id", inputId)
|
|
||||||
.prop("tabindex", definitionAttr.position)
|
|
||||||
.prop("attribute-id", valueAttr.isOwned ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
|
|
||||||
.prop("attribute-type", valueAttr.type)
|
|
||||||
.prop("attribute-name", valueAttr.name)
|
|
||||||
.prop("value", valueAttr.value)
|
|
||||||
.addClass("form-control")
|
|
||||||
.addClass("promoted-attribute-input")
|
|
||||||
.change(promotedAttributeChanged);
|
|
||||||
|
|
||||||
idx++;
|
|
||||||
|
|
||||||
const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
|
|
||||||
|
|
||||||
const $actionCell = $("<td>");
|
|
||||||
const $multiplicityCell = $("<td>");
|
|
||||||
|
|
||||||
$tr
|
|
||||||
.append($labelCell)
|
|
||||||
.append($inputCell)
|
|
||||||
.append($actionCell)
|
|
||||||
.append($multiplicityCell);
|
|
||||||
|
|
||||||
if (valueAttr.type === 'label') {
|
|
||||||
if (definition.labelType === 'text') {
|
|
||||||
$input.prop("type", "text");
|
|
||||||
|
|
||||||
// no need to await for this, can be done asynchronously
|
|
||||||
server.get('attributes/values/' + encodeURIComponent(valueAttr.name)).then(attributeValues => {
|
|
||||||
if (attributeValues.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeValues = attributeValues.map(attribute => { return { value: attribute }; });
|
|
||||||
|
|
||||||
$input.autocomplete({
|
|
||||||
appendTo: document.querySelector('body'),
|
|
||||||
hint: false,
|
|
||||||
autoselect: true,
|
|
||||||
openOnFocus: true,
|
|
||||||
minLength: 0
|
|
||||||
}, [{
|
|
||||||
displayKey: 'value',
|
|
||||||
source: function (term, cb) {
|
|
||||||
term = term.toLowerCase();
|
|
||||||
|
|
||||||
const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term));
|
|
||||||
|
|
||||||
cb(filtered);
|
|
||||||
}
|
|
||||||
}]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (definition.labelType === 'number') {
|
|
||||||
$input.prop("type", "number");
|
|
||||||
}
|
|
||||||
else if (definition.labelType === 'boolean') {
|
|
||||||
$input.prop("type", "checkbox");
|
|
||||||
|
|
||||||
if (valueAttr.value === "true") {
|
|
||||||
$input.prop("checked", "checked");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (definition.labelType === 'date') {
|
|
||||||
$input.prop("type", "date");
|
|
||||||
|
|
||||||
const $todayButton = $("<button>").addClass("btn btn-sm").text("Today").click(() => {
|
|
||||||
$input.val(utils.formatDateISO(new Date()));
|
|
||||||
$input.trigger("change");
|
|
||||||
});
|
|
||||||
|
|
||||||
$actionCell.append($todayButton);
|
|
||||||
}
|
|
||||||
else if (definition.labelType === 'url') {
|
|
||||||
$input.prop("placeholder", "http://website...");
|
|
||||||
|
|
||||||
const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => {
|
|
||||||
window.open($input.val(), '_blank');
|
|
||||||
});
|
|
||||||
|
|
||||||
$actionCell.append($openButton);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
messagingService.logError("Unknown labelType=" + definitionAttr.labelType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (valueAttr.type === 'relation') {
|
|
||||||
if (valueAttr.value) {
|
|
||||||
$input.val(await treeUtils.getNoteTitle(valueAttr.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
// no need to wait for this
|
|
||||||
noteAutocompleteService.initNoteAutocomplete($input);
|
|
||||||
|
|
||||||
$input.on('autocomplete:selected', function(event, suggestion, dataset) {
|
|
||||||
promotedAttributeChanged(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
$input.prop("data-selected-path", valueAttr.value);
|
|
||||||
|
|
||||||
// ideally we'd use link instead of button which would allow tooltip preview, but
|
|
||||||
// we can't guarantee updating the link in the a element
|
|
||||||
const $openButton = $("<button>").addClass("btn btn-sm").text("Open").click(() => {
|
|
||||||
const notePath = $input.prop("data-selected-path");
|
|
||||||
|
|
||||||
treeService.activateNote(notePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
$actionCell.append($openButton);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
messagingService.logError("Unknown attribute type=" + valueAttr.type);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (definition.multiplicityType === "multivalue") {
|
|
||||||
const addButton = $("<span>")
|
|
||||||
.addClass("glyphicon glyphicon-plus pointer")
|
|
||||||
.prop("title", "Add new attribute")
|
|
||||||
.click(async () => {
|
|
||||||
const $new = await createRow(definitionAttr, {
|
|
||||||
attributeId: "",
|
|
||||||
type: valueAttr.type,
|
|
||||||
name: definitionAttr.name,
|
|
||||||
value: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
$tr.after($new);
|
|
||||||
|
|
||||||
$new.find('input').focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeButton = $("<span>")
|
|
||||||
.addClass("glyphicon glyphicon-trash pointer")
|
|
||||||
.prop("title", "Remove this attribute")
|
|
||||||
.click(async () => {
|
|
||||||
if (valueAttr.attributeId) {
|
|
||||||
await server.remove("notes/" + noteId + "/attributes/" + valueAttr.attributeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$tr.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
$multiplicityCell.append(addButton).append(" ").append(removeButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $tr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (promoted.length > 0) {
|
|
||||||
const $tbody = $("<tbody>");
|
|
||||||
|
|
||||||
for (const definitionAttr of promoted) {
|
|
||||||
const definitionType = definitionAttr.type;
|
|
||||||
const valueType = definitionType.substr(0, definitionType.length - 11);
|
|
||||||
|
|
||||||
let valueAttrs = attributes.filter(el => el.name === definitionAttr.name && el.type === valueType);
|
|
||||||
|
|
||||||
if (valueAttrs.length === 0) {
|
|
||||||
valueAttrs.push({
|
|
||||||
attributeId: "",
|
|
||||||
type: valueType,
|
|
||||||
name: definitionAttr.name,
|
|
||||||
value: ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (definitionAttr.value.multiplicityType === 'singlevalue') {
|
|
||||||
valueAttrs = valueAttrs.slice(0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const valueAttr of valueAttrs) {
|
|
||||||
const $tr = await createRow(definitionAttr, valueAttr);
|
|
||||||
|
|
||||||
$tbody.append($tr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we replace the whole content in one step so there can't be any race conditions
|
|
||||||
// (previously we saw promoted attributes doubling)
|
|
||||||
$promotedAttributesContainer.empty().append($tbody);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$attributeListInner.empty();
|
|
||||||
|
|
||||||
if (attributes.length > 0) {
|
|
||||||
for (const attribute of attributes) {
|
|
||||||
if (attribute.type === 'label') {
|
|
||||||
$attributeListInner.append(utils.formatLabel(attribute) + " ");
|
|
||||||
}
|
|
||||||
else if (attribute.type === 'relation') {
|
|
||||||
if (attribute.value) {
|
|
||||||
$attributeListInner.append('@' + attribute.name + "=");
|
|
||||||
$attributeListInner.append(await linkService.createNoteLink(attribute.value));
|
|
||||||
$attributeListInner.append(" ");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
messagingService.logError(`Relation ${attribute.attributeId} has empty target`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (attribute.type === 'label-definition' || attribute.type === 'relation-definition') {
|
|
||||||
$attributeListInner.append(attribute.name + " definition ");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
messagingService.logError("Unknown attr type: " + attribute.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$attributeList.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function promotedAttributeChanged(event) {
|
|
||||||
const $attr = $(event.target);
|
|
||||||
|
|
||||||
let value;
|
|
||||||
|
|
||||||
if ($attr.prop("type") === "checkbox") {
|
|
||||||
value = $attr.is(':checked') ? "true" : "false";
|
|
||||||
}
|
|
||||||
else if ($attr.prop("attribute-type") === "relation") {
|
|
||||||
const selectedPath = $attr.prop("data-selected-path");
|
|
||||||
|
|
||||||
if (selectedPath) {
|
|
||||||
value = treeUtils.getNoteIdFromNotePath(selectedPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
value = $attr.val();
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await server.put("notes/" + getCurrentNoteId() + "/attribute", {
|
|
||||||
attributeId: $attr.prop("attribute-id"),
|
|
||||||
type: $attr.prop("attribute-type"),
|
|
||||||
name: $attr.prop("attribute-name"),
|
|
||||||
value: value
|
|
||||||
});
|
|
||||||
|
|
||||||
$attr.prop("attribute-id", result.attributeId);
|
|
||||||
|
|
||||||
infoService.showMessage("Attribute has been saved.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadNote(noteId) {
|
async function loadNote(noteId) {
|
||||||
const row = await server.get('notes/' + noteId);
|
const row = await server.get('notes/' + noteId);
|
||||||
|
|
||||||
@ -590,9 +306,6 @@ export default {
|
|||||||
getCurrentNoteId,
|
getCurrentNoteId,
|
||||||
newNoteCreated,
|
newNoteCreated,
|
||||||
focusOnTitle,
|
focusOnTitle,
|
||||||
getAttributes,
|
|
||||||
showAttributes,
|
|
||||||
refreshAttributes,
|
|
||||||
saveNote,
|
saveNote,
|
||||||
saveNoteIfChanged,
|
saveNoteIfChanged,
|
||||||
noteChanged,
|
noteChanged,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import bundleService from "./bundle.js";
|
import bundleService from "./bundle.js";
|
||||||
import server from "./server.js";
|
import server from "./server.js";
|
||||||
import noteDetailService from "./note_detail.js";
|
import noteDetailService from "./note_detail.js";
|
||||||
|
import attributeService from "./attributes.js";
|
||||||
|
|
||||||
const $component = $('#note-detail-render');
|
const $component = $('#note-detail-render');
|
||||||
const $noteDetailRenderHelp = $('#note-detail-render-help');
|
const $noteDetailRenderHelp = $('#note-detail-render-help');
|
||||||
@ -8,7 +9,7 @@ const $noteDetailRenderContent = $('#note-detail-render-content');
|
|||||||
const $renderButton = $('#render-button');
|
const $renderButton = $('#render-button');
|
||||||
|
|
||||||
async function render() {
|
async function render() {
|
||||||
const attributes = await noteDetailService.getAttributes();
|
const attributes = await attributeService.getAttributes();
|
||||||
const renderNotes = attributes.filter(attr =>
|
const renderNotes = attributes.filter(attr =>
|
||||||
attr.type === 'relation'
|
attr.type === 'relation'
|
||||||
&& attr.name === 'renderNote'
|
&& attr.name === 'renderNote'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user