@@ -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,71 @@ import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromT
3537
3638const CONFIG = `roam/js/attribute-select` ;
3739
40+ const TEMPLATE_MAP = {
41+ "No styling" : {
42+ transform : ( text : string ) => text ,
43+ description : "No styling"
44+ } ,
45+ "Remove Double Brackets" : {
46+ transform : ( text : string ) => text . replace ( / \[ \[ ( .* ?) \] \] / g, '$1' ) ,
47+ description : "Removes [[text]] format"
48+ } ,
49+ "Convert to Uppercase" : {
50+ transform : ( text : string ) => text . toUpperCase ( ) ,
51+ description : "Makes text all caps"
52+ } ,
53+ "Capitalize Words" : {
54+ transform : ( text : string ) => text . split ( ' ' ) . map ( word => {
55+ if ( ! word ) return '' ;
56+ return word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) . toLowerCase ( ) ;
57+ } ) . join ( ' ' ) ,
58+ description : "Makes first letter of each word uppercase"
59+ } ,
60+ "Custom Format" : {
61+ transform : ( text : string , pattern ?: string , replacement ?: string ) => {
62+ if ( ! pattern ) return text ;
63+ try {
64+ const regex = new RegExp ( pattern ) ;
65+ return text . replace ( regex , replacement || '' ) ;
66+ } catch ( e ) {
67+ console . error ( "Invalid regex:" , e ) ;
68+ return text ;
69+ }
70+ } ,
71+ description : "Apply custom regex pattern"
72+ }
73+ } as const ;
74+
75+ type TemplateName = keyof typeof TEMPLATE_MAP ;
76+
77+ type FormatParams = {
78+ text : string ;
79+ templateName : string ;
80+ customPattern ?: string ;
81+ customReplacement ?: string ;
82+ } ;
83+
84+ const applyFormatting = ( {
85+ text,
86+ templateName,
87+ customPattern,
88+ customReplacement
89+ } : FormatParams ) : string => {
90+ try {
91+ const template = TEMPLATE_MAP [ templateName as TemplateName ] ;
92+ if ( ! template ) return text ;
93+
94+ if ( templateName === "Custom Format" && customPattern ) {
95+ return template . transform ( text , customPattern , customReplacement ) ;
96+ }
97+
98+ return template . transform ( text ) ;
99+ } catch ( e ) {
100+ console . error ( "Error in transform function:" , e ) ;
101+ return text ;
102+ }
103+ } ;
104+
38105type AttributeButtonPopoverProps < T > = {
39106 items : T [ ] ;
40107 onItemSelect ?: ( selectedItem : T ) => void ;
@@ -59,18 +126,55 @@ const AttributeButtonPopover = <T extends ReactText>({
59126 return String ( item ) . toLowerCase ( ) . includes ( query . toLowerCase ( ) ) ;
60127 } ;
61128 const [ sliderValue , setSliderValue ] = useState ( 0 ) ;
129+
62130 useEffect ( ( ) => {
63131 setSliderValue ( Number ( currentValue ) ) ;
64132 } , [ isOpen , currentValue ] ) ;
133+
134+ const formatConfig = useMemo ( ( ) => {
135+ try {
136+ const configUid = getPageUidByPageTitle ( CONFIG ) ;
137+ const attributesNode = getSubTree ( {
138+ key : "attributes" ,
139+ parentUid : configUid ,
140+ } ) ;
141+ const attributeUid = getSubTree ( {
142+ key : attributeName ,
143+ parentUid : attributesNode . uid ,
144+ } ) . uid ;
145+
146+ return {
147+ templateName : getSettingValueFromTree ( {
148+ key : "template" ,
149+ parentUid : attributeUid ,
150+ } ) || "No styling" ,
151+
152+ customPattern : getSettingValueFromTree ( {
153+ key : "customPattern" ,
154+ parentUid : attributeUid ,
155+ } ) ,
156+
157+ customReplacement : getSettingValueFromTree ( {
158+ key : "customReplacement" ,
159+ parentUid : attributeUid ,
160+ } )
161+ } ;
162+ } catch ( e ) {
163+ console . error ( "Error getting format config:" , e ) ;
164+ return {
165+ templateName : "No styling" ,
166+ customPattern : undefined ,
167+ customReplacement : undefined
168+ } ;
169+ }
170+ } , [ attributeName ] ) ;
65171
66- 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 ;
73- } ;
172+ const formatText = useMemo ( ( ) =>
173+ ( text : string ) => applyFormatting ( {
174+ text,
175+ ...formatConfig
176+ } ) ,
177+ [ formatConfig ] ) ;
74178
75179 // Only show filter if we have more than 10 items
76180 const shouldFilter = filterable && items . length > 10 ;
@@ -82,7 +186,7 @@ const AttributeButtonPopover = <T extends ReactText>({
82186 items = { items }
83187 activeItem = { currentValue as T }
84188 filterable = { shouldFilter }
85- // transformItem={(item) => formatDisplayText (String(item))}
189+ transformItem = { ( item ) => formatText ( String ( item ) ) }
86190 onItemSelect = { ( s ) => {
87191 updateBlock ( {
88192 text : `${ attributeName } :: ${ s } ` ,
@@ -474,6 +578,35 @@ const TabsPanel = ({
474578 const [ optionType , setOptionType ] = useState ( initialOptionType || "text" ) ;
475579 const [ min , setMin ] = useState ( Number ( rangeNode . children [ 0 ] ?. text ) || 0 ) ;
476580 const [ max , setMax ] = useState ( Number ( rangeNode . children [ 1 ] ?. text ) || 10 ) ;
581+
582+ const { initialTemplate, initialCustomPattern, initialCustomReplacement } = useMemo ( ( ) => {
583+ const savedTemplate = getSettingValueFromTree ( {
584+ key : "template" ,
585+ parentUid : attributeUid ,
586+ } ) || "No styling" ;
587+
588+ const savedCustomPattern = getSettingValueFromTree ( {
589+ key : "customPattern" ,
590+ parentUid : attributeUid ,
591+ } ) || "" ;
592+
593+ const savedCustomReplacement = getSettingValueFromTree ( {
594+ key : "customReplacement" ,
595+ parentUid : attributeUid ,
596+ } ) || "" ;
597+
598+ return {
599+ initialTemplate : savedTemplate ,
600+ initialCustomPattern : savedCustomPattern ,
601+ initialCustomReplacement : savedCustomReplacement
602+ } ;
603+ } , [ attributeUid ] ) ;
604+
605+ const [ selectedTemplate , setSelectedTemplate ] = useState ( initialTemplate ) ;
606+ const [ customPattern , setCustomPattern ] = useState ( initialCustomPattern ) ;
607+ const [ customReplacement , setCustomReplacement ] = useState ( initialCustomReplacement ) ;
608+ const [ isValidRegex , setIsValidRegex ] = useState ( true ) ;
609+
477610
478611 // For a better UX replace renderBlock with a controlled list
479612 // add Edit, Delete, and Add New buttons
@@ -570,16 +703,135 @@ const TabsPanel = ({
570703 </ FormGroup >
571704
572705 { optionType === "text" && (
573- < Button
574- intent = "primary"
575- text = { "Find All Current Values" }
576- rightIcon = { "search" }
577- onClick = { ( ) => {
578- const potentialOptions = findAllPotentialOptions ( attributeName ) ;
579- setPotentialOptions ( potentialOptions ) ;
580- setShowPotentialOptions ( true ) ;
581- } }
582- />
706+ < >
707+ < FormGroup
708+ label = {
709+ < div className = "flex items-center gap-2" >
710+ < span > Display Format</ span >
711+ < Tooltip
712+ content = "Select a template or use a custom regex pattern"
713+ placement = "top"
714+ >
715+ < Icon icon = "info-sign" className = "opacity-80" />
716+ </ Tooltip >
717+ </ div >
718+ }
719+ className = "m-0"
720+ >
721+ < div className = "flex flex-col space-y-2" >
722+ < div className = "flex items-center gap-2" >
723+ < MenuItemSelect
724+ items = { Object . keys ( TEMPLATE_MAP ) }
725+ onItemSelect = { ( template ) => {
726+ setSelectedTemplate ( template ) ;
727+ setInputSetting ( {
728+ blockUid : attributeUid ,
729+ key : "template" ,
730+ value : template ,
731+ } ) ;
732+ } }
733+ activeItem = { selectedTemplate }
734+ />
735+ < Tooltip
736+ content = {
737+ < div className = "text-sm" >
738+ < p className = "font-bold mb-2" > Available Templates:</ p >
739+ < ul className = "list-disc list-inside space-y-1" >
740+ { Object . entries ( TEMPLATE_MAP ) . map ( ( [ name , { description } ] ) => (
741+ < li key = { name } >
742+ < span className = "font-mono" > { name } :</ span > { " " }
743+ { description }
744+ </ li >
745+ ) ) }
746+ </ ul >
747+ </ div >
748+ }
749+ placement = "top"
750+ >
751+ < Icon icon = "info-sign" className = "opacity-80" />
752+ </ Tooltip >
753+ </ div >
754+
755+ { selectedTemplate === "Custom Format" && (
756+ < div className = "space-y-2" >
757+ < FormGroup label = "Pattern (regex)" className = "m-0" >
758+ < input
759+ className = { `bp3-input font-mono text-sm w-full ${ ! isValidRegex ? 'bp3-intent-danger' : '' } ` }
760+ placeholder = "E.g., \[\[(.*?)\]\]"
761+ value = { customPattern }
762+ onChange = { ( e ) => {
763+ const newValue = e . target . value ;
764+ setCustomPattern ( newValue ) ;
765+ try {
766+ if ( newValue ) new RegExp ( newValue ) ;
767+ setIsValidRegex ( true ) ;
768+ } catch ( e ) {
769+ setIsValidRegex ( false ) ;
770+ }
771+ setInputSetting ( {
772+ blockUid : attributeUid ,
773+ key : "customPattern" ,
774+ value : newValue ,
775+ } ) ;
776+ } }
777+ />
778+ { ! isValidRegex && (
779+ < div className = "text-red-500 text-xs mt-1" >
780+ Invalid regular expression
781+ </ div >
782+ ) }
783+ </ FormGroup >
784+
785+ < FormGroup label = "Replacement" className = "m-0" >
786+ < input
787+ className = "bp3-input font-mono text-sm w-full"
788+ placeholder = "E.g., $1"
789+ value = { customReplacement }
790+ onChange = { ( e ) => {
791+ const newValue = e . target . value ;
792+ setCustomReplacement ( newValue ) ;
793+ setInputSetting ( {
794+ blockUid : attributeUid ,
795+ key : "customReplacement" ,
796+ value : newValue ,
797+ } ) ;
798+ } }
799+ />
800+ </ FormGroup >
801+
802+ < div className = "bg-gray-100 p-2 rounded text-sm" >
803+ < div className = "font-bold mb-1" > Preview:</ div >
804+ < div >
805+ < span className = "font-bold" > Input:</ span > < span className = "font-mono" > [[Example]]</ span >
806+ </ div >
807+ < div >
808+ < span className = "font-bold" > Output:</ span > < span className = "font-mono" >
809+ { customPattern ?
810+ applyFormatting ( {
811+ text : "[[Example]]" ,
812+ templateName : "Custom Format" ,
813+ customPattern,
814+ customReplacement
815+ } ) :
816+ "[[Example]]" }
817+ </ span >
818+ </ div >
819+ </ div >
820+ </ div >
821+ ) }
822+ </ div >
823+ </ FormGroup >
824+ < Button
825+ intent = "primary"
826+ text = { "Find All Current Values" }
827+ rightIcon = { "search" }
828+ onClick = { ( ) => {
829+ const potentialOptions = findAllPotentialOptions ( attributeName ) ;
830+ setPotentialOptions ( potentialOptions ) ;
831+ setShowPotentialOptions ( true ) ;
832+ } }
833+ />
834+ </ >
583835 ) }
584836 < div
585837 className = "flex items-start space-x-4"
@@ -750,6 +1002,7 @@ export const toggleFeature = async (flag: boolean) => {
7501002 .inline-menu-item-select > span > div {display:inline}
7511003 #attribute-select-config .rm-block-separator {display: none;}
7521004 ` ) ;
1005+
7531006 definedAttributes = getDefinedAttributes ( ) ;
7541007 const pageUid =
7551008 getPageUidByPageTitle ( CONFIG ) ||
0 commit comments