@@ -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,45 @@ const AttributeButtonPopover = <T extends ReactText>({
5972 return String ( item ) . toLowerCase ( ) . includes ( query . toLowerCase ( ) ) ;
6073 } ;
6174 const [ sliderValue , setSliderValue ] = useState ( 0 ) ;
75+ const [ transformFunction , setTransformFunction ] = useState < string > ( "" ) ;
76+
6277 useEffect ( ( ) => {
6378 setSliderValue ( Number ( currentValue ) ) ;
6479 } , [ isOpen , currentValue ] ) ;
6580
81+ useEffect ( ( ) => {
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+ } ) ;
95+ setTransformFunction ( format || "text => text" ) ;
96+ } , [ attributeName ] ) ;
97+
6698 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 ;
99+ if ( ! transformFunction ) return text ;
100+ try {
101+ const transform = new Function ( "text" , `
102+ try {
103+ return (${ transformFunction } )(text);
104+ } catch (e) {
105+ console.error("Error in transform function:", e);
106+ return text;
107+ }
108+ ` ) as ( text : string ) => string ;
109+ return transform ( text ) ;
110+ } catch ( e ) {
111+ console . error ( "Invalid transformation function:" , e ) ;
112+ return text ;
113+ }
73114 } ;
74115
75116 // Only show filter if we have more than 10 items
@@ -82,7 +123,7 @@ const AttributeButtonPopover = <T extends ReactText>({
82123 items = { items }
83124 activeItem = { currentValue as T }
84125 filterable = { shouldFilter }
85- // transformItem={(item) => formatDisplayText(String(item))}
126+ transformItem = { ( item ) => formatDisplayText ( String ( item ) ) }
86127 onItemSelect = { ( s ) => {
87128 updateBlock ( {
88129 text : `${ attributeName } :: ${ s } ` ,
@@ -471,6 +512,33 @@ const TabsPanel = ({
471512 const [ optionType , setOptionType ] = useState ( initialOptionType || "text" ) ;
472513 const [ min , setMin ] = useState ( Number ( rangeNode . children [ 0 ] ?. text ) || 0 ) ;
473514 const [ max , setMax ] = useState ( Number ( rangeNode . children [ 1 ] ?. text ) || 10 ) ;
515+
516+ const { initialFormat, initialTemplate } = useMemo ( ( ) => {
517+ const savedFormat = getSettingValueFromTree ( {
518+ key : "format" ,
519+ parentUid : attributeUid ,
520+ } ) || "text => text" ;
521+
522+ const savedTemplate = getSettingValueFromTree ( {
523+ key : "template" ,
524+ parentUid : attributeUid ,
525+ } ) ;
526+
527+ if ( savedTemplate ) {
528+ return { initialFormat : savedFormat , initialTemplate : savedTemplate } ;
529+ }
530+
531+ const matchingTemplate = Object . entries ( TEMPLATE_MAP ) . find (
532+ ( [ _ , func ] ) => func === savedFormat
533+ ) ;
534+ return {
535+ initialFormat : savedFormat ,
536+ initialTemplate : matchingTemplate ? matchingTemplate [ 0 ] : "No styling"
537+ } ;
538+ } , [ attributeUid ] ) ;
539+
540+ const [ transformFunction , setTransformFunction ] = useState ( initialFormat ) ;
541+ const [ selectedTemplate , setSelectedTemplate ] = useState ( initialTemplate ) ;
474542
475543 // For a better UX replace renderBlock with a controlled list
476544 // add Edit, Delete, and Add New buttons
@@ -567,16 +635,99 @@ const TabsPanel = ({
567635 </ FormGroup >
568636
569637 { 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- />
638+ < >
639+ < FormGroup
640+ label = {
641+ < div className = "flex items-center gap-2" >
642+ < span > Display Format</ span >
643+ < Tooltip
644+ content = "JavaScript function that takes 'text' as input and returns formatted string"
645+ placement = "top"
646+ >
647+ < Icon icon = "info-sign" className = "opacity-80" />
648+ </ Tooltip >
649+ </ div >
650+ }
651+ className = "m-0"
652+ >
653+ < div className = "flex flex-col space-y-2" >
654+ < div className = "flex items-center gap-2" >
655+ < MenuItemSelect
656+ items = { Object . keys ( TEMPLATE_MAP ) }
657+ onItemSelect = { ( template ) => {
658+ const newFunction = TEMPLATE_MAP [ template as TemplateName ] ;
659+ setSelectedTemplate ( template ) ;
660+ setTransformFunction ( newFunction ) ;
661+ setInputSetting ( {
662+ blockUid : attributeUid ,
663+ key : "format" ,
664+ value : newFunction ,
665+ } ) ;
666+ setInputSetting ( {
667+ blockUid : attributeUid ,
668+ key : "template" ,
669+ value : template ,
670+ } ) ;
671+ } }
672+ activeItem = { selectedTemplate }
673+ />
674+ < Tooltip
675+ content = {
676+ < div className = "text-sm" >
677+ < p className = "font-bold mb-2" > Available Templates:</ p >
678+ < ul className = "list-disc list-inside space-y-1" >
679+ { Object . entries ( TEMPLATE_MAP ) . map ( ( [ name , func ] ) => (
680+ < li key = { name } >
681+ < span className = "font-mono" > { name } :</ span > { " " }
682+ { name === "Remove Double Brackets" && "Removes [[text]] format" }
683+ { name === "Convert to Uppercase" && "Makes text all caps" }
684+ { name === "Add Prefix" && "Adds \"Status:\" before text" }
685+ { name === "Capitalize Words" && "Makes first letter of each word uppercase" }
686+ { name === "Custom Format" && "Start with a blank template" }
687+ { name === "No styling" && "No styling" }
688+ </ li >
689+ ) ) }
690+ </ ul >
691+ </ div >
692+ }
693+ placement = "top"
694+ >
695+ < Icon icon = "info-sign" className = "opacity-80" />
696+ </ Tooltip >
697+ </ div >
698+ < textarea
699+ className = "bp3-input font-mono text-sm min-h-[100px] p-2 resize-y"
700+ placeholder = "text => text"
701+ value = { transformFunction }
702+ onChange = { ( e ) => {
703+ const newValue = e . target . value ;
704+ setTransformFunction ( newValue ) ;
705+ setSelectedTemplate ( "Custom Format" ) ;
706+ setInputSetting ( {
707+ blockUid : attributeUid ,
708+ key : "format" ,
709+ value : newValue ,
710+ } ) ;
711+ setInputSetting ( {
712+ blockUid : attributeUid ,
713+ key : "template" ,
714+ value : "Custom Format" ,
715+ } ) ;
716+ } }
717+ />
718+ </ div >
719+ </ FormGroup >
720+ < Button
721+ intent = "primary"
722+ text = { "Find All Current Values" }
723+ rightIcon = { "search" }
724+ onClick = { ( ) => {
725+ const potentialOptions = findAllPotentialOptions ( attributeName ) ;
726+ setPotentialOptions ( potentialOptions ) ;
727+ setShowPotentialOptions ( true ) ;
728+ } }
729+ />
730+ </ >
580731 ) }
581732 < div
582733 className = "flex items-start space-x-4"
0 commit comments