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 ;
}
2025-02-22 00:53:24 +02:00
2020-07-25 00:06:49 +02:00
. promoted - attributes - container {
2024-10-20 00:38:17 +03:00
margin : 0 1.5 em ;
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 ;
2024-10-20 00:38:17 +03:00
display : table ;
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 ;
2024-10-20 00:38:17 +03:00
display : table - row ;
}
2024-10-20 01:11:29 +03:00
. promoted - attribute - cell > label {
user - select : none ;
font - weight : bold ;
2024-11-27 21:26:07 +02:00
vertical - align : middle ;
2024-10-20 01:11:29 +03:00
}
2024-10-20 00:38:17 +03:00
. promoted - attribute - cell > * {
display : table - cell ;
padding : 1 px 0 ;
2020-07-25 00:06:49 +02:00
}
2025-02-22 00:53:24 +02:00
2020-07-25 00:06:49 +02:00
. promoted - attribute - cell div . input - group {
margin - left : 10 px ;
2024-10-20 00:38:17 +03:00
display : flex ;
2024-11-27 21:22:50 +02:00
min - height : 40 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
}
2024-10-19 22:40:27 +03:00
. promoted - attribute - cell input [ type = "checkbox" ] {
2024-11-27 21:26:07 +02:00
width : 22 px ! important ;
2024-10-20 00:38:17 +03:00
flex - grow : 0 ;
width : unset ;
2024-10-19 22:40:27 +03:00
}
2025-02-22 00:53:24 +02:00
2020-01-13 20:25:56 +01:00
< / s t y l e >
2025-02-22 00:53:24 +02:00
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 ,
2025-01-09 18:07:02 +02:00
activate : options . is ( "promotedAttributesOpenInRibbon" ) ,
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 ) {
2025-01-09 18:07:02 +02:00
const valueType = definitionAttr . name . startsWith ( "label:" ) ? "label" : "relation" ;
2020-08-12 00:02:19 +02:00
const valueName = definitionAttr . name . substr ( valueType . length + 1 ) ;
2020-01-13 20:25:56 +01:00
2025-01-09 18:07:02 +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
2025-01-09 18:07:02 +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 ( ) ;
2024-12-01 17:38:13 +02:00
const id = ` value- ${ valueAttr . attributeId } ` ;
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 )
2024-10-20 01:11:29 +03:00
. prop ( "id" , id )
2025-01-09 18:07:02 +02: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
2021-11-26 23:39:08 +01:00
. attr ( "data-attribute-type" , valueAttr . type )
. attr ( "data-attribute-name" , valueAttr . name )
2020-01-13 20:25:56 +01:00
. prop ( "value" , valueAttr . value )
2025-02-22 10:05:14 +02:00
. prop ( "placeholder" , t ( "promoted_attributes.unset-field-placeholder" ) )
2020-01-13 20:25:56 +01:00
. addClass ( "form-control" )
. addClass ( "promoted-attribute-input" )
2025-01-09 18:07:02 +02:00
. on ( "change" , ( event ) => this . promotedAttributeChanged ( event ) ) ;
2020-01-13 20:25:56 +01:00
2020-07-25 00:06:49 +02:00
const $actionCell = $ ( "<div>" ) ;
2025-01-09 18:07:02 +02:00
const $multiplicityCell = $ ( "<td>" ) . addClass ( "multiplicity" ) . attr ( "nowrap" , true ) ;
2020-01-13 20:25:56 +01:00
2020-07-25 00:06:49 +02:00
const $wrapper = $ ( '<div class="promoted-attribute-cell">' )
2025-01-09 18:07:02 +02:00
. append (
$ ( "<label>" )
. prop ( "for" , id )
. 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 ) ;
2025-01-09 18:07:02 +02:00
if ( valueAttr . type === "label" ) {
if ( definition . labelType === "text" ) {
2020-01-13 20:25:56 +01:00
$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
2025-01-09 18:07:02 +02:00
server . get ( ` attribute-values/ ${ encodeURIComponent ( valueAttr . name ) } ` ) . then ( ( attributeValues ) => {
2023-10-08 22:58:31 +03:00
if ( attributeValues . length === 0 ) {
return ;
2020-01-13 20:25:56 +01:00
}
2020-06-14 16:04:00 +02:00
2025-01-09 18:07:02 +02:00
attributeValues = attributeValues . map ( ( attribute ) => ( { value : attribute } ) ) ;
$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 ) ) ;
2023-10-08 22:58:31 +03:00
} ) ;
}
2025-01-09 18:07:02 +02:00
} else if ( definition . labelType === "number" ) {
2020-01-13 20:25:56 +01:00
$input . prop ( "type" , "number" ) ;
let step = 1 ;
for ( let i = 0 ; i < ( definition . numberPrecision || 0 ) && i < 10 ; i ++ ) {
step /= 10 ;
}
$input . prop ( "step" , step ) ;
2025-01-09 18:07:02 +02:00
$input . css ( "text-align" , "right" ) . css ( "width" , "120" ) ;
} else if ( definition . labelType === "boolean" ) {
2020-01-13 20:25:56 +01:00
$input . prop ( "type" , "checkbox" ) ;
2025-02-22 01:30:55 +02:00
$input . wrap ( $ ( ` <label class="tn-checkbox"></label> ` ) ) ;
$wrapper . find ( ".input-group" ) . removeClass ( "input-group" ) ;
2020-01-13 20:25:56 +01:00
if ( valueAttr . value === "true" ) {
$input . prop ( "checked" , "checked" ) ;
}
2025-01-09 18:07:02 +02:00
} else if ( definition . labelType === "date" ) {
2020-01-13 20:25:56 +01:00
$input . prop ( "type" , "date" ) ;
2025-01-09 18:07:02 +02:00
} else if ( definition . labelType === "datetime" ) {
$input . prop ( "type" , "datetime-local" ) ;
} else if ( definition . labelType === "time" ) {
$input . prop ( "type" , "time" ) ;
} 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" ) )
2025-01-09 18:07:02 +02:00
. on ( "click" , ( ) => window . open ( $input . val ( ) , "_blank" ) ) ;
2020-01-13 20:25:56 +01:00
2024-09-03 18:42:03 +02:00
$input . after ( $openButton ) ;
2025-01-09 18:07:02 +02: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
}
2025-01-09 18:07:02 +02:00
} else if ( valueAttr . type === "relation" ) {
2020-01-13 20:25:56 +01:00
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
2025-01-09 18:07:02 +02:00
$input . on ( "autocomplete:noteselected" , ( event , suggestion , dataset ) => {
2023-10-08 22:58:31 +03:00
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" ) ;
}
2025-01-09 18:07:02 +02: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>" )
2025-02-22 00:53:24 +02:00
. addClass ( "bx bx-plus pointer tn-tool-button" )
2024-08-06 10:12:53 +08:00
. prop ( "title" , t ( "promoted_attributes.add_new_attribute" ) )
2025-01-09 18:07:02 +02:00
. on ( "click" , async ( ) => {
const $new = await this . createPromotedAttributeCell (
definitionAttr ,
{
attributeId : "" ,
type : valueAttr . type ,
name : valueName ,
value : ""
} ,
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
2025-01-09 18:07:02 +02:00
$new . find ( "input" ) . trigger ( "focus" ) ;
2020-01-13 20:25:56 +01:00
} ) ;
2021-11-26 23:39:08 +01:00
const $removeButton = $ ( "<span>" )
2025-02-22 00:53:24 +02:00
. addClass ( "bx bx-trash pointer tn-tool-button" )
2024-08-06 10:12:53 +08:00
. prop ( "title" , t ( "promoted_attributes.remove_this_attribute" ) )
2025-01-09 18:07:02 +02: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 ) {
2025-01-09 18:07:02 +02:00
const $new = await this . createPromotedAttributeCell (
definitionAttr ,
{
attributeId : "" ,
type : valueAttr . type ,
name : valueName ,
value : ""
} ,
valueName
) ;
2021-11-26 23:39:08 +01:00
$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
} ) ;
2025-01-09 18:07:02 +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" ) {
2025-01-09 18:07:02 +02:00
value = $attr . is ( ":checked" ) ? "true" : "false" ;
} 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 ) : "" ;
2025-01-09 18:07:02 +02:00
} else {
2020-01-13 20:25:56 +01:00
value = $attr . val ( ) ;
}
2025-01-09 18:07:02 +02:00
const result = await server . put (
` notes/ ${ this . noteId } /attribute ` ,
{
attributeId : $attr . attr ( "data-attribute-id" ) ,
type : $attr . attr ( "data-attribute-type" ) ,
name : $attr . attr ( "data-attribute-name" ) ,
value : value
} ,
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 } ) {
2025-01-09 18:07: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
}