Notes/src/public/app/widgets/promoted_attributes.js

274 lines
9.6 KiB
JavaScript
Raw Normal View History

2020-01-13 20:25:56 +01:00
import server from "../services/server.js";
import ws from "../services/ws.js";
2020-01-25 09:56:08 +01:00
import treeService from "../services/tree.js";
2020-01-13 20:25:56 +01:00
import noteAutocompleteService from "../services/note_autocomplete.js";
import TabAwareWidget from "./tab_aware_widget.js";
const TPL = `
<div class="promoted-attributes-wrapper">
<style>
.promoted-attributes-wrapper {
margin: auto;
/* setting the display to block since "table" doesn't support scrolling */
display: block;
/** flex-basis: content; - use once "content" is implemented by chrome */
flex-shrink: 0;
flex-grow: 0;
overflow: auto;
}
.promoted-attributes td, .promoted-attributes th {
padding: 5px;
min-width: 50px; /* otherwise checkboxes can collapse into 0 width (if there are only checkboxes) */
2020-01-13 20:25:56 +01:00
}
</style>
<table class="promoted-attributes"></table>
</div>
`;
export default class PromotedAttributesWidget extends TabAwareWidget {
doRender() {
2020-01-18 18:01:16 +01:00
this.$widget = $(TPL);
2020-01-13 20:25:56 +01:00
2020-01-18 18:01:16 +01:00
this.$container = this.$widget.find(".promoted-attributes");
2020-01-13 20:25:56 +01:00
}
async refreshWithNote(note) {
2020-01-13 20:25:56 +01:00
this.$container.empty();
const attributes = note.getAttributes();
2020-01-13 20:25:56 +01:00
const promoted = attributes
2020-07-01 00:02:13 +02:00
.filter(attr => attr.isDefinition())
.filter(attr => {
2020-07-01 00:02:13 +02:00
const def = attr.getDefinition();
2020-07-01 00:02:13 +02:00
return def && def.isPromoted;
});
2020-01-13 20:25:56 +01:00
2020-07-01 00:02:13 +02:00
if (promoted.length > 0 && !note.hasLabel('hidePromotedAttributes')) {
2020-01-13 20:25:56 +01:00
const $tbody = $("<tbody>");
for (const definitionAttr of promoted) {
2020-07-01 00:02:13 +02:00
const definitionType = definitionAttr.name.startsWith('label:') ? 'label' : 'relation';
const valueName = definitionAttr.name.substr(definitionType.length + 1);
2020-01-13 20:25:56 +01:00
2020-07-17 23:55:59 +02:00
let valueAttrs = attributes.filter(el => el.name === valueName && el.type === 'label');
2020-01-13 20:25:56 +01:00
if (valueAttrs.length === 0) {
valueAttrs.push({
attributeId: "",
2020-07-01 00:02:13 +02:00
type: definitionType,
name: valueName,
2020-01-13 20:25:56 +01:00
value: ""
});
}
2020-07-01 00:02:13 +02:00
if (definitionAttr.value.multiplicity === 'single') {
2020-01-13 20:25:56 +01:00
valueAttrs = valueAttrs.slice(0, 1);
}
for (const valueAttr of valueAttrs) {
2020-07-01 00:02:13 +02:00
const $tr = await this.createPromotedAttributeRow(definitionAttr, valueAttr, valueName);
2020-01-13 20:25:56 +01:00
$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)
this.$container.empty().append($tbody);
2020-03-06 22:17:07 +01:00
this.toggleInt(true);
2020-01-19 10:29:21 +01:00
}
else {
2020-03-06 22:17:07 +01:00
this.toggleInt(false);
2020-01-13 20:25:56 +01:00
}
return attributes;
}
2020-07-01 00:02:13 +02:00
async createPromotedAttributeRow(definitionAttr, valueAttr, valueName) {
const definition = definitionAttr.getDefinition();
2020-01-13 20:25:56 +01:00
const $tr = $("<tr>");
2020-07-01 00:02:13 +02:00
const $labelCell = $("<th>").append(valueName);
2020-01-13 20:25:56 +01:00
const $input = $("<input>")
2020-06-18 23:53:57 +02:00
.prop("tabindex", 200 + definitionAttr.position)
.prop("attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : '') // if not owned, we'll force creation of a new attribute instead of updating the inherited one
2020-01-13 20:25:56 +01:00
.prop("attribute-type", valueAttr.type)
.prop("attribute-name", valueAttr.name)
.prop("value", valueAttr.value)
.addClass("form-control")
.addClass("promoted-attribute-input")
.on('change', event => this.promotedAttributeChanged(event));
const $inputCell = $("<td>").append($("<div>").addClass("input-group").append($input));
const $actionCell = $("<td>");
const $multiplicityCell = $("<td>")
.addClass("multiplicity")
.attr("nowrap", true);
$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 => ({ value: attribute }));
2020-01-13 20:25:56 +01:00
$input.autocomplete({
appendTo: document.querySelector('body'),
hint: false,
autoselect: false,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
}, [{
displayKey: 'value',
source: function (term, cb) {
term = term.toLowerCase();
const filtered = attributeValues.filter(attr => attr.value.toLowerCase().includes(term));
cb(filtered);
}
}]);
$input.on('autocomplete:selected', e => this.promotedAttributeChanged(e))
2020-01-13 20:25:56 +01:00
});
}
else if (definition.labelType === 'number') {
$input.prop("type", "number");
let step = 1;
for (let i = 0; i < (definition.numberPrecision || 0) && i < 10; i++) {
step /= 10;
}
$input.prop("step", step);
}
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");
}
else if (definition.labelType === 'url') {
$input.prop("placeholder", "http://website...");
const $openButton = $("<span>")
.addClass("input-group-text open-external-link-button bx bx-trending-up")
.prop("title", "Open external link")
.on('click', () => window.open($input.val(), '_blank'));
$input.after($("<div>")
.addClass("input-group-append")
.append($openButton));
}
else {
ws.logError("Unknown labelType=" + definitionAttr.labelType);
}
}
else if (valueAttr.type === 'relation') {
if (valueAttr.value) {
2020-01-25 09:56:08 +01:00
$input.val(await treeService.getNoteTitle(valueAttr.value));
2020-01-13 20:25:56 +01:00
}
// no need to wait for this
noteAutocompleteService.initNoteAutocomplete($input);
$input.on('autocomplete:selected', (event, suggestion, dataset) => {
this.promotedAttributeChanged(event);
});
2020-05-16 22:11:09 +02:00
$input.setSelectedNotePath(valueAttr.value);
2020-01-13 20:25:56 +01:00
}
else {
ws.logError("Unknown attribute type=" + valueAttr.type);
return;
}
2020-07-01 00:02:13 +02:00
if (definition.multiplicity === "multivalue") {
2020-01-13 20:25:56 +01:00
const addButton = $("<span>")
.addClass("bx bx-plus pointer")
.prop("title", "Add new attribute")
.on('click', async () => {
const $new = await this.createPromotedAttributeRow(definitionAttr, {
attributeId: "",
type: valueAttr.type,
name: definitionAttr.name,
value: ""
});
$tr.after($new);
$new.find('input').trigger('focus');
});
const removeButton = $("<span>")
.addClass("bx bx-trash pointer")
.prop("title", "Remove this attribute")
.on('click', async () => {
if (valueAttr.attributeId) {
await server.remove("notes/" + this.noteId + "/attributes/" + valueAttr.attributeId, this.componentId);
2020-01-13 20:25:56 +01:00
}
$tr.remove();
});
$multiplicityCell.append(addButton).append(" &nbsp;").append(removeButton);
}
return $tr;
}
async 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") {
2020-05-16 22:11:09 +02:00
const selectedPath = $attr.getSelectedNotePath();
2020-01-13 20:25:56 +01:00
2020-01-25 09:56:08 +01:00
value = selectedPath ? treeService.getNoteIdFromNotePath(selectedPath) : "";
2020-01-13 20:25:56 +01:00
}
else {
value = $attr.val();
}
2020-02-03 21:56:45 +01:00
const result = await server.put(`notes/${this.noteId}/attribute`, {
2020-01-13 20:25:56 +01:00
attributeId: $attr.prop("attribute-id"),
type: $attr.prop("attribute-type"),
name: $attr.prop("attribute-name"),
value: value
}, this.componentId);
2020-01-13 20:25:56 +01:00
$attr.prop("attribute-id", result.attributeId);
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) {
this.refresh();
}
}
}