Skip to content

Commit b1fc8f3

Browse files
committed
add formating option
1 parent 2477fe7 commit b1fc8f3

1 file changed

Lines changed: 163 additions & 17 deletions

File tree

src/features/attributeSelect.tsx

Lines changed: 163 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
NumericInput,
1313
Slider,
1414
Popover,
15+
Icon,
16+
Tooltip,
1517
} from "@blueprintjs/core";
1618
import { Select } from "@blueprintjs/select";
1719
import createHTMLObserver from "roamjs-components/dom/createHTMLObserver";
@@ -35,6 +37,17 @@ import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromT
3537

3638
const 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+
3851
type 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

Comments
 (0)