@@ -12,6 +12,8 @@ import {
1212 NumericInput ,
1313 Slider ,
1414 Popover ,
15+ Icon ,
16+ Tooltip ,
1517} from "@blueprintjs/core" ;
1618import { Select } from "@blueprintjs/select" ;
1719import createHTMLObserver from "roamjs-components/dom/createHTMLObserver" ;
@@ -35,6 +37,17 @@ import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromT
3537
3638const CONFIG = `roam/js/attribute-select` ;
3739
40+ const TEMPLATE_MAP = {
41+ "No styling" : "text => text" ,
42+ "Remove Double Brackets" : `text => text.replace(/^\\[\\[(.*?)\\]\\]$/g, '$1')` ,
43+ "Convert to Uppercase" : "text => text.toUpperCase()" ,
44+ "Add Prefix" : "text => `Status: ${text}`" ,
45+ "Capitalize Words" : "text => text.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')" ,
46+ "Custom Format" : "text => text"
47+ } as const ;
48+
49+ type TemplateName = keyof typeof TEMPLATE_MAP ;
50+
3851type AttributeButtonPopoverProps < T > = {
3952 items : T [ ] ;
4053 onItemSelect ?: ( selectedItem : T ) => void ;
@@ -59,17 +72,40 @@ const AttributeButtonPopover = <T extends ReactText>({
5972 return String ( item ) . toLowerCase ( ) . includes ( query . toLowerCase ( ) ) ;
6073 } ;
6174 const [ sliderValue , setSliderValue ] = useState ( 0 ) ;
75+
6276 useEffect ( ( ) => {
6377 setSliderValue ( Number ( currentValue ) ) ;
6478 } , [ isOpen , currentValue ] ) ;
6579
6680 const formatDisplayText = ( text : string ) : string => {
67- // TODO: for doantrang982/eng-77-decouple-display-from-output: Create formatDisplayText from configPage
68- // const match = text.match(/\[\[(.*?)\]\]/);
69- // if (match && match[1]) {
70- // return match[1];
71- // }
72- return text ;
81+ try {
82+ const configUid = getPageUidByPageTitle ( CONFIG ) ;
83+ const attributesNode = getSubTree ( {
84+ key : "attributes" ,
85+ parentUid : configUid ,
86+ } ) ;
87+ const attributeUid = getSubTree ( {
88+ key : attributeName ,
89+ parentUid : attributesNode . uid ,
90+ } ) . uid ;
91+ const format = getSettingValueFromTree ( {
92+ key : "format" ,
93+ parentUid : attributeUid ,
94+ } ) || "text => text" ;
95+
96+ const transform = new Function ( "text" , `
97+ try {
98+ return (${ format } )(text);
99+ } catch (e) {
100+ console.error("Error in transform function:", e);
101+ return text;
102+ }
103+ ` ) as ( text : string ) => string ;
104+ return transform ( text ) ;
105+ } catch ( e ) {
106+ console . error ( "Invalid transformation function:" , e ) ;
107+ return text ;
108+ }
73109 } ;
74110
75111 // Only show filter if we have more than 10 items
@@ -82,7 +118,7 @@ const AttributeButtonPopover = <T extends ReactText>({
82118 items = { items }
83119 activeItem = { currentValue as T }
84120 filterable = { shouldFilter }
85- // transformItem={(item) => formatDisplayText(String(item))}
121+ transformItem = { ( item ) => formatDisplayText ( String ( item ) ) }
86122 onItemSelect = { ( s ) => {
87123 updateBlock ( {
88124 text : `${ attributeName } :: ${ s } ` ,
@@ -471,6 +507,33 @@ const TabsPanel = ({
471507 const [ optionType , setOptionType ] = useState ( initialOptionType || "text" ) ;
472508 const [ min , setMin ] = useState ( Number ( rangeNode . children [ 0 ] ?. text ) || 0 ) ;
473509 const [ max , setMax ] = useState ( Number ( rangeNode . children [ 1 ] ?. text ) || 10 ) ;
510+
511+ const { initialFormat, initialTemplate } = useMemo ( ( ) => {
512+ const savedFormat = getSettingValueFromTree ( {
513+ key : "format" ,
514+ parentUid : attributeUid ,
515+ } ) || "text => text" ;
516+
517+ const savedTemplate = getSettingValueFromTree ( {
518+ key : "template" ,
519+ parentUid : attributeUid ,
520+ } ) ;
521+
522+ if ( savedTemplate ) {
523+ return { initialFormat : savedFormat , initialTemplate : savedTemplate } ;
524+ }
525+
526+ const matchingTemplate = Object . entries ( TEMPLATE_MAP ) . find (
527+ ( [ _ , func ] ) => func === savedFormat
528+ ) ;
529+ return {
530+ initialFormat : savedFormat ,
531+ initialTemplate : matchingTemplate ? matchingTemplate [ 0 ] : "No styling"
532+ } ;
533+ } , [ attributeUid ] ) ;
534+
535+ const [ transformFunction , setTransformFunction ] = useState ( initialFormat ) ;
536+ const [ selectedTemplate , setSelectedTemplate ] = useState ( initialTemplate ) ;
474537
475538 // For a better UX replace renderBlock with a controlled list
476539 // add Edit, Delete, and Add New buttons
@@ -567,16 +630,99 @@ const TabsPanel = ({
567630 </ FormGroup >
568631
569632 { optionType === "text" && (
570- < Button
571- intent = "primary"
572- text = { "Find All Current Values" }
573- rightIcon = { "search" }
574- onClick = { ( ) => {
575- const potentialOptions = findAllPotentialOptions ( attributeName ) ;
576- setPotentialOptions ( potentialOptions ) ;
577- setShowPotentialOptions ( true ) ;
578- } }
579- />
633+ < >
634+ < FormGroup
635+ label = {
636+ < div className = "flex items-center gap-2" >
637+ < span > Display Format</ span >
638+ < Tooltip
639+ content = "JavaScript function that takes 'text' as input and returns formatted string"
640+ placement = "top"
641+ >
642+ < Icon icon = "info-sign" className = "opacity-80" />
643+ </ Tooltip >
644+ </ div >
645+ }
646+ className = "m-0"
647+ >
648+ < div className = "flex flex-col space-y-2" >
649+ < div className = "flex items-center gap-2" >
650+ < MenuItemSelect
651+ items = { Object . keys ( TEMPLATE_MAP ) }
652+ onItemSelect = { ( template ) => {
653+ const newFunction = TEMPLATE_MAP [ template as TemplateName ] ;
654+ setSelectedTemplate ( template ) ;
655+ setTransformFunction ( newFunction ) ;
656+ setInputSetting ( {
657+ blockUid : attributeUid ,
658+ key : "format" ,
659+ value : newFunction ,
660+ } ) ;
661+ setInputSetting ( {
662+ blockUid : attributeUid ,
663+ key : "template" ,
664+ value : template ,
665+ } ) ;
666+ } }
667+ activeItem = { selectedTemplate }
668+ />
669+ < Tooltip
670+ content = {
671+ < div className = "text-sm" >
672+ < p className = "font-bold mb-2" > Available Templates:</ p >
673+ < ul className = "list-disc list-inside space-y-1" >
674+ { Object . entries ( TEMPLATE_MAP ) . map ( ( [ name , func ] ) => (
675+ < li key = { name } >
676+ < span className = "font-mono" > { name } :</ span > { " " }
677+ { name === "Remove Double Brackets" && "Removes [[text]] format" }
678+ { name === "Convert to Uppercase" && "Makes text all caps" }
679+ { name === "Add Prefix" && "Adds \"Status:\" before text" }
680+ { name === "Capitalize Words" && "Makes first letter of each word uppercase" }
681+ { name === "Custom Format" && "Start with a blank template" }
682+ { name === "No styling" && "No styling" }
683+ </ li >
684+ ) ) }
685+ </ ul >
686+ </ div >
687+ }
688+ placement = "top"
689+ >
690+ < Icon icon = "info-sign" className = "opacity-80" />
691+ </ Tooltip >
692+ </ div >
693+ < textarea
694+ className = "bp3-input font-mono text-sm min-h-[100px] p-2 resize-y"
695+ placeholder = "text => text"
696+ value = { transformFunction }
697+ onChange = { ( e ) => {
698+ const newValue = e . target . value ;
699+ setTransformFunction ( newValue ) ;
700+ setSelectedTemplate ( "Custom Format" ) ;
701+ setInputSetting ( {
702+ blockUid : attributeUid ,
703+ key : "format" ,
704+ value : newValue ,
705+ } ) ;
706+ setInputSetting ( {
707+ blockUid : attributeUid ,
708+ key : "template" ,
709+ value : "Custom Format" ,
710+ } ) ;
711+ } }
712+ />
713+ </ div >
714+ </ FormGroup >
715+ < Button
716+ intent = "primary"
717+ text = { "Find All Current Values" }
718+ rightIcon = { "search" }
719+ onClick = { ( ) => {
720+ const potentialOptions = findAllPotentialOptions ( attributeName ) ;
721+ setPotentialOptions ( potentialOptions ) ;
722+ setShowPotentialOptions ( true ) ;
723+ } }
724+ />
725+ </ >
580726 ) }
581727 < div
582728 className = "flex items-start space-x-4"
0 commit comments