2024-08-06 10:12:53 +08:00
import { t } from "../../services/i18n.js" ;
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" ;
2021-05-22 12:35:41 +02:00
import NoteContextAwareWidget from "../note_context_aware_widget.js" ;
2021-08-25 22:49:24 +02:00
import attributeService from "../../services/attributes.js" ;
2023-08-09 22:50:41 +02:00
import options from "../../services/options.js" ;
2023-10-08 22:58:31 +03:00
import utils from "../../services/utils.js" ;
2020-01-13 20:25:56 +01:00
const TPL = `
2023-11-24 00:04:49 +01:00
< div class = "promoted-attributes-widget" >
2020-01-13 20:25:56 +01:00
< style >
2023-11-24 00:04:49 +01:00
body . mobile . promoted - attributes - widget {
/* https://github.com/zadam/trilium/issues/4468 */
flex - shrink : 0.4 ;
overflow : auto ;
}
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
}
2023-04-08 14:56:37 +08:00
. promoted - attribute - cell strong {
word - break : keep - all ;
2023-09-22 04:58:06 -04:00
white - space : nowrap ;
2023-04-08 14:56:37 +08:00
}
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 >
2023-10-08 22:58:31 +03:00
< / d i v > ` ;
2020-01-13 20:25:56 +01:00
2023-10-08 22:58:31 +03:00
/ * *
* This widget is quite special because it ' s used in the desktop ribbon , but in mobile outside of ribbon .
* This works without many issues ( apart from autocomplete ) , but it should be kept in mind when changing things
* and testing .
* /
2021-05-22 12:35:41 +02:00
export default class PromotedAttributesWidget extends NoteContextAwareWidget {
2021-06-27 12:53:05 +02:00
get name ( ) {
return "promotedAttributes" ;
}
2021-07-05 09:44:41 +02:00
get toggleCommand ( ) {
return "toggleRibbonTabPromotedAttributes" ;
}
2020-01-13 20:25:56 +01:00
doRender ( ) {
2020-01-18 18:01:16 +01:00
this . $widget = $ ( TPL ) ;
2021-06-13 22:55:31 +02:00
this . contentSized ( ) ;
2020-07-25 00:06:49 +02:00
this . $container = this . $widget . find ( ".promoted-attributes-container" ) ;
2020-01-13 20:25:56 +01:00
}
2021-05-23 23:41:01 +02:00
getTitle ( note ) {
2021-01-23 21:41:02 +01:00
const promotedDefAttrs = note . getPromotedDefinitionAttributes ( ) ;
2020-11-19 23:02:25 +01:00
if ( promotedDefAttrs . length === 0 ) {
2024-09-03 18:42:03 +02:00
return { show : false } ;
2020-11-19 23:02:25 +01:00
}
return {
show : true ,
2023-08-09 22:50:41 +02:00
activate : options . is ( 'promotedAttributesOpenInRibbon' ) ,
2024-08-06 10:12:53 +08:00
title : t ( 'promoted_attributes.promoted_attributes' ) ,
2021-05-23 23:41:01 +02:00
icon : "bx bx-table"
2020-11-19 23:02:25 +01:00
} ;
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 ( ) ;
2021-01-23 21:41:02 +01:00
const promotedDefAttrs = note . getPromotedDefinitionAttributes ( ) ;
2020-08-12 00:02:19 +02:00
const ownedAttributes = note . getOwnedAttributes ( ) ;
2023-07-20 23:22:31 +02:00
// attrs are not resorted if position changes after the initial load
// promoted attrs are sorted primarily by order of definitions, but with multi-valued promoted attrs
// the order of attributes is important as well
2023-11-03 01:11:47 +01:00
ownedAttributes . sort ( ( a , b ) => a . position - b . position ) ;
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
2023-06-30 11:18:34 +02:00
// we replace the whole content in one step, so there can't be any race conditions
2020-07-25 23:24:48 +02:00
// (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-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 )
2021-11-26 23:39:08 +01:00
. attr ( "data-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
. attr ( "data-attribute-type" , valueAttr . type )
. attr ( "data-attribute-name" , valueAttr . name )
2020-01-13 20:25:56 +01:00
. 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">' )
2023-09-22 04:58:06 -04:00
. append ( $ ( "<strong>" ) . text ( definition . promotedAlias ? ? valueName ) )
2020-07-25 00:06:49 +02:00
. 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" ) ;
2023-10-08 22:58:31 +03:00
// autocomplete for label values is just nice to have, mobile can keep labels editable without autocomplete
if ( utils . isDesktop ( ) ) {
// no need to await for this, can be done asynchronously
server . get ( ` attribute-values/ ${ encodeURIComponent ( valueAttr . name ) } ` ) . then ( attributeValues => {
if ( attributeValues . length === 0 ) {
return ;
2020-01-13 20:25:56 +01:00
}
2020-06-14 16:04:00 +02:00
2024-09-03 18:42:03 +02:00
attributeValues = attributeValues . map ( attribute => ( { value : attribute } ) ) ;
2023-10-08 22:58:31 +03: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 ) ;
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" ) ;
}
2023-07-10 17:24:34 +05:30
else if ( definition . labelType === 'datetime' ) {
$input . prop ( 'type' , 'datetime-local' )
}
2020-01-13 20:25:56 +01:00
else if ( definition . labelType === 'url' ) {
2024-08-06 10:12:53 +08:00
$input . prop ( "placeholder" , t ( "promoted_attributes.url_placeholder" ) ) ;
2020-01-13 20:25:56 +01:00
const $openButton = $ ( "<span>" )
2020-09-03 22:22:21 +02:00
. addClass ( "input-group-text open-external-link-button bx bx-window-open" )
2024-08-06 10:12:53 +08:00
. prop ( "title" , t ( "promoted_attributes.open_external_link" ) )
2020-01-13 20:25:56 +01:00
. on ( 'click' , ( ) => window . open ( $input . val ( ) , '_blank' ) ) ;
2024-09-03 18:42:03 +02:00
$input . after ( $openButton ) ;
2020-01-13 20:25:56 +01:00
}
else {
2024-08-06 10:12:53 +08:00
ws . logError ( t ( "promoted_attributes.unknown_label_type" , { type : definitionAttr . labelType } ) ) ;
2020-01-13 20:25:56 +01:00
}
}
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
}
2023-10-08 22:58:31 +03:00
if ( utils . isDesktop ( ) ) {
// no need to wait for this
2024-09-03 18:42:03 +02:00
noteAutocompleteService . initNoteAutocomplete ( $input , { allowCreatingNotes : true } ) ;
2020-01-13 20:25:56 +01:00
2023-10-08 22:58:31 +03:00
$input . on ( 'autocomplete:noteselected' , ( event , suggestion , dataset ) => {
this . promotedAttributeChanged ( event ) ;
} ) ;
2020-01-13 20:25:56 +01:00
2023-10-08 22:58:31 +03:00
$input . setSelectedNotePath ( valueAttr . value ) ;
} else {
// we can't provide user a way to edit the relation so make it read only
$input . attr ( "readonly" , "readonly" ) ;
}
2020-01-13 20:25:56 +01:00
}
else {
2024-09-03 18:42:03 +02:00
ws . logError ( t ( ` promoted_attributes.unknown_attribute_type ` , { type : valueAttr . type } ) ) ;
2020-01-13 20:25:56 +01:00
return ;
}
2020-08-14 00:11:26 +02:00
if ( definition . multiplicity === "multi" ) {
2021-11-26 23:39:08 +01:00
const $addButton = $ ( "<span>" )
2020-01-13 20:25:56 +01:00
. addClass ( "bx bx-plus pointer" )
2024-08-06 10:12:53 +08:00
. prop ( "title" , t ( "promoted_attributes.add_new_attribute" ) )
2020-01-13 20:25:56 +01:00
. 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' ) ;
} ) ;
2021-11-26 23:39:08 +01:00
const $removeButton = $ ( "<span>" )
2020-01-13 20:25:56 +01:00
. addClass ( "bx bx-trash pointer" )
2024-08-06 10:12:53 +08:00
. prop ( "title" , t ( "promoted_attributes.remove_this_attribute" ) )
2020-01-13 20:25:56 +01:00
. on ( 'click' , async ( ) => {
2021-11-26 23:39:08 +01:00
const attributeId = $input . attr ( "data-attribute-id" ) ;
if ( attributeId ) {
2022-12-21 15:19:05 +01:00
await server . remove ( ` notes/ ${ this . noteId } /attributes/ ${ attributeId } ` , this . componentId ) ;
2021-11-26 23:39:08 +01:00
}
// if it's the last one the create new empty form immediately
const sameAttrSelector = ` input[data-attribute-type=' ${ valueAttr . type } '][data-attribute-name=' ${ valueName } '] ` ;
if ( this . $widget . find ( sameAttrSelector ) . length <= 1 ) {
const $new = await this . createPromotedAttributeCell ( definitionAttr , {
attributeId : "" ,
type : valueAttr . type ,
name : valueName ,
value : ""
} , valueName ) ;
$wrapper . after ( $new ) ;
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 ( " " )
2021-11-26 23:39:08 +01:00
. append ( $addButton )
2020-08-14 00:11:26 +02:00
. append ( " " )
2021-11-26 23:39:08 +01:00
. 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" ;
}
2021-11-26 23:39:08 +01:00
else if ( $attr . attr ( "data-attribute-type" ) === "relation" ) {
2020-05-16 22:11:09 +02:00
const selectedPath = $attr . getSelectedNotePath ( ) ;
2020-01-13 20:25:56 +01:00
2023-05-29 22:37:19 +02:00
value = selectedPath ? treeService . getNoteIdFromUrl ( 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 ` , {
2021-11-26 23:39:08 +01:00
attributeId : $attr . attr ( "data-attribute-id" ) ,
type : $attr . attr ( "data-attribute-type" ) ,
name : $attr . attr ( "data-attribute-name" ) ,
2020-01-13 20:25:56 +01:00
value : value
2020-06-08 00:29:52 +02:00
} , this . componentId ) ;
2020-01-13 20:25:56 +01:00
2021-11-26 23:39:08 +01:00
$attr . attr ( "data-attribute-id" , result . attributeId ) ;
2020-01-13 20:25:56 +01:00
}
2020-06-07 23:57:10 +02:00
2022-02-01 20:24:58 +01:00
focus ( ) {
this . $widget . find ( ".promoted-attribute-input:first" ) . focus ( ) ;
}
2024-09-03 18:42:03 +02:00
entitiesReloadedEvent ( { loadResults } ) {
2023-06-05 16:12:02 +02:00
if ( loadResults . getAttributeRows ( this . componentId ) . find ( attr => attributeService . isAffecting ( attr , this . note ) ) ) {
2020-06-07 23:57:10 +02:00
this . refresh ( ) ;
}
}
2020-05-11 23:57:39 +02:00
}