Skip to content

Commit e666721

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

File tree

1 file changed

+168
-17
lines changed

1 file changed

+168
-17
lines changed

src/features/attributeSelect.tsx

Lines changed: 168 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,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

Comments
 (0)