2020-11-26 23:00:27 +01:00
import server from "../../services/server.js" ;
import ws from "../../services/ws.js" ;
import treeService from "../../services/tree.js" ;
import noteAutocompleteService from "../../services/note_autocomplete.js" ;
import TabAwareWidget from "../tab_aware_widget.js" ;
2020-01-13 20:25:56 +01:00
const TPL = `
2020-07-25 00:06:49 +02:00
< div >
2020-01-13 20:25:56 +01:00
< style >
2020-07-25 00:06:49 +02:00
. promoted - attributes - container {
2020-01-13 20:25:56 +01:00
margin : auto ;
2020-07-25 00:06:49 +02:00
display : flex ;
flex - direction : row ;
2020-01-13 20:25:56 +01:00
flex - shrink : 0 ;
flex - grow : 0 ;
2020-07-25 00:06:49 +02:00
justify - content : space - evenly ;
2020-01-13 20:25:56 +01:00
overflow : auto ;
2020-07-23 22:31:06 +02:00
max - height : 400 px ;
2020-07-25 00:06:49 +02:00
flex - wrap : wrap ;
2020-01-13 20:25:56 +01:00
}
2020-07-25 00:06:49 +02:00
. promoted - attribute - cell {
display : flex ;
align - items : center ;
margin : 10 px ;
}
. promoted - attribute - cell div . input - group {
margin - left : 10 px ;
2020-01-13 20:25:56 +01:00
}
< / s t y l e >
2020-07-25 00:06:49 +02:00
< div class = "promoted-attributes-container" > < / d i v >
2020-01-13 20:25:56 +01:00
< / d i v >
` ;
export default class PromotedAttributesWidget extends TabAwareWidget {
doRender ( ) {
2020-01-18 18:01:16 +01:00
this . $widget = $ ( TPL ) ;
2020-08-30 22:39:15 +02:00
this . overflowing ( ) ;
2020-07-25 00:06:49 +02:00
this . $container = this . $widget . find ( ".promoted-attributes-container" ) ;
2020-11-19 23:02:25 +01:00
this . $title = $ ( '<div>' ) ;
2020-01-13 20:25:56 +01:00
}
2020-11-19 23:02:25 +01:00
renderTitle ( note ) {
const promotedDefAttrs = this . getPromotedDefinitionAttributes ( ) ;
if ( promotedDefAttrs . length === 0 ) {
return { show : false } ;
}
this . $title . text ( ` Promoted attrs ( ${ promotedDefAttrs . length } ) ` ) ;
return {
show : true ,
activate : true ,
$title : this . $title
} ;
2020-10-30 22:57:26 +01:00
}
2020-01-25 14:37:12 +01:00
async refreshWithNote ( note ) {
2020-01-13 20:25:56 +01:00
this . $container . empty ( ) ;
2020-08-12 00:02:19 +02:00
const promotedDefAttrs = this . getPromotedDefinitionAttributes ( ) ;
const ownedAttributes = note . getOwnedAttributes ( ) ;
2020-01-13 20:25:56 +01:00
2020-08-12 00:02:19 +02:00
if ( promotedDefAttrs . length === 0 ) {
2020-07-25 23:24:48 +02:00
this . toggleInt ( false ) ;
return ;
}
2020-01-13 20:25:56 +01:00
2020-08-14 00:11:26 +02:00
const $cells = [ ] ;
2020-08-12 00:02:19 +02:00
for ( const definitionAttr of promotedDefAttrs ) {
const valueType = definitionAttr . name . startsWith ( 'label:' ) ? 'label' : 'relation' ;
const valueName = definitionAttr . name . substr ( valueType . length + 1 ) ;
2020-01-13 20:25:56 +01:00
2020-08-12 00:02:19 +02:00
let valueAttrs = ownedAttributes . filter ( el => el . name === valueName && el . type === valueType ) ;
2020-01-13 20:25:56 +01:00
2020-07-25 23:24:48 +02:00
if ( valueAttrs . length === 0 ) {
valueAttrs . push ( {
attributeId : "" ,
2020-08-12 00:02:19 +02:00
type : valueType ,
2020-07-25 23:24:48 +02:00
name : valueName ,
value : ""
} ) ;
}
2020-01-13 20:25:56 +01:00
2020-08-14 00:11:26 +02:00
if ( definitionAttr . getDefinition ( ) . multiplicity === 'single' ) {
2020-07-25 23:24:48 +02:00
valueAttrs = valueAttrs . slice ( 0 , 1 ) ;
2020-01-13 20:25:56 +01:00
}
2020-07-25 23:24:48 +02:00
for ( const valueAttr of valueAttrs ) {
const $cell = await this . createPromotedAttributeCell ( definitionAttr , valueAttr , valueName ) ;
2020-08-14 00:11:26 +02:00
$cells . push ( $cell ) ;
2020-07-25 23:24:48 +02:00
}
2020-01-19 10:29:21 +01:00
}
2020-07-25 23:24:48 +02:00
// we replace the whole content in one step so there can't be any race conditions
// (previously we saw promoted attributes doubling)
2020-08-14 00:11:26 +02:00
this . $container . empty ( ) . append ( ... $cells ) ;
2020-07-25 23:24:48 +02:00
this . toggleInt ( true ) ;
}
2020-08-12 00:02:19 +02:00
getPromotedDefinitionAttributes ( ) {
2020-07-25 23:24:48 +02:00
if ( this . note . hasLabel ( 'hidePromotedAttributes' ) ) {
return [ ] ;
2020-01-13 20:25:56 +01:00
}
2020-07-25 23:24:48 +02:00
return this . note . getAttributes ( )
. filter ( attr => attr . isDefinition ( ) )
. filter ( attr => {
const def = attr . getDefinition ( ) ;
return def && def . isPromoted ;
} ) ;
2020-01-13 20:25:56 +01:00
}
2020-07-25 00:06:49 +02:00
async createPromotedAttributeCell ( definitionAttr , valueAttr , valueName ) {
2020-07-01 00:02:13 +02:00
const definition = definitionAttr . getDefinition ( ) ;
2020-07-25 00:06:49 +02:00
2020-01-13 20:25:56 +01:00
const $input = $ ( "<input>" )
2020-06-18 23:53:57 +02:00
. prop ( "tabindex" , 200 + definitionAttr . position )
2020-05-11 23:57:39 +02:00
. 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 ) ) ;
2020-07-25 00:06:49 +02:00
const $actionCell = $ ( "<div>" ) ;
2020-01-13 20:25:56 +01:00
const $multiplicityCell = $ ( "<td>" )
. addClass ( "multiplicity" )
. attr ( "nowrap" , true ) ;
2020-07-25 00:06:49 +02:00
const $wrapper = $ ( '<div class="promoted-attribute-cell">' )
. append ( $ ( "<strong>" ) . text ( valueName ) )
. append ( $ ( "<div>" ) . addClass ( "input-group" ) . append ( $input ) )
2020-01-13 20:25:56 +01:00
. 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 ;
}
2020-02-28 22:07:08 +01:00
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 ) ;
}
} ] ) ;
2020-06-14 16:04:00 +02:00
2020-10-28 21:48:34 +01:00
$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 ) ;
2020-09-03 22:22:21 +02:00
$input
. css ( "text-align" , "right" )
. css ( "width" , "120" ) ;
2020-01-13 20:25:56 +01:00
}
else if ( definition . labelType === 'boolean' ) {
$input . prop ( "type" , "checkbox" ) ;
2020-10-19 20:22:30 +02:00
// hack, without this the checkbox is invisible
// we should be using a different bootstrap structure for checkboxes
$input . css ( 'width' , '80px' ) ;
2020-01-13 20:25:56 +01:00
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>" )
2020-09-03 22:22:21 +02:00
. addClass ( "input-group-text open-external-link-button bx bx-window-open" )
2020-01-13 20:25:56 +01:00
. 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 ) ;
2020-09-21 22:08:54 +02:00
$input . on ( 'autocomplete:noteselected' , ( event , suggestion , dataset ) => {
2020-01-13 20:25:56 +01:00
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-08-14 00:11:26 +02:00
if ( definition . multiplicity === "multi" ) {
2020-01-13 20:25:56 +01:00
const addButton = $ ( "<span>" )
. addClass ( "bx bx-plus pointer" )
. prop ( "title" , "Add new attribute" )
. on ( 'click' , async ( ) => {
2020-07-25 00:06:49 +02:00
const $new = await this . createPromotedAttributeCell ( definitionAttr , {
2020-01-13 20:25:56 +01:00
attributeId : "" ,
type : valueAttr . type ,
2020-08-14 00:11:26 +02:00
name : valueName ,
2020-01-13 20:25:56 +01:00
value : ""
2020-08-14 00:11:26 +02:00
} , valueName ) ;
2020-01-13 20:25:56 +01:00
2020-07-25 00:06:49 +02:00
$wrapper . after ( $new ) ;
2020-01-13 20:25:56 +01:00
$new . find ( 'input' ) . trigger ( 'focus' ) ;
} ) ;
const removeButton = $ ( "<span>" )
. addClass ( "bx bx-trash pointer" )
. prop ( "title" , "Remove this attribute" )
. on ( 'click' , async ( ) => {
if ( valueAttr . attributeId ) {
2020-06-08 00:29:52 +02:00
await server . remove ( "notes/" + this . noteId + "/attributes/" + valueAttr . attributeId , this . componentId ) ;
2020-01-13 20:25:56 +01:00
}
2020-07-25 00:06:49 +02:00
$wrapper . remove ( ) ;
2020-01-13 20:25:56 +01:00
} ) ;
2020-08-14 00:11:26 +02:00
$multiplicityCell
. append ( " " )
. append ( addButton )
. append ( " " )
. append ( removeButton ) ;
2020-01-13 20:25:56 +01:00
}
2020-07-25 00:06:49 +02:00
return $wrapper ;
2020-01-13 20:25:56 +01:00
}
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
2020-06-08 00:29:52 +02:00
} , this . componentId ) ;
2020-01-13 20:25:56 +01:00
$attr . prop ( "attribute-id" , result . attributeId ) ;
}
2020-06-07 23:57:10 +02:00
2020-06-09 22:59:22 +02:00
entitiesReloadedEvent ( { loadResults } ) {
if ( loadResults . getAttributes ( this . componentId ) . find ( attr => attr . isAffecting ( this . note ) ) ) {
2020-06-07 23:57:10 +02:00
this . refresh ( ) ;
2020-11-19 23:02:25 +01:00
this . renderTitle ( this . note ) ;
2020-06-07 23:57:10 +02:00
}
}
2020-05-11 23:57:39 +02:00
}