diff --git a/src/features/attributeSelect.tsx b/src/features/attributeSelect.tsx index 09bcb4d..274f626 100644 --- a/src/features/attributeSelect.tsx +++ b/src/features/attributeSelect.tsx @@ -12,6 +12,8 @@ import { NumericInput, Slider, Popover, + Icon, + Tooltip, } from "@blueprintjs/core"; import { Select } from "@blueprintjs/select"; import createHTMLObserver from "roamjs-components/dom/createHTMLObserver"; @@ -35,6 +37,71 @@ import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromT const CONFIG = `roam/js/attribute-select`; +const TEMPLATE_MAP = { + "No styling": { + transform: (text: string) => text, + description: "No styling" + }, + "Remove Double Brackets": { + transform: (text: string) => text.replace(/\[\[(.*?)\]\]/g, '$1'), + description: "Removes [[text]] format" + }, + "Convert to Uppercase": { + transform: (text: string) => text.toUpperCase(), + description: "Makes text all caps" + }, + "Capitalize Words": { + transform: (text: string) => text.split(' ').map(word => { + if (!word) return ''; + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }).join(' '), + description: "Makes first letter of each word uppercase" + }, + "Custom Format": { + transform: (text: string, pattern?: string, replacement?: string) => { + if (!pattern) return text; + try { + const regex = new RegExp(pattern); + return text.replace(regex, replacement || ''); + } catch (e) { + console.error("Invalid regex:", e); + return text; + } + }, + description: "Apply custom regex pattern" + } +} as const; + +type TemplateName = keyof typeof TEMPLATE_MAP; + +type FormatParams = { + text: string; + templateName: string; + customPattern?: string; + customReplacement?: string; +}; + +const applyFormatting = ({ + text, + templateName, + customPattern, + customReplacement +}: FormatParams): string => { + try { + const template = TEMPLATE_MAP[templateName as TemplateName]; + if (!template) return text; + + if (templateName === "Custom Format" && customPattern) { + return template.transform(text, customPattern, customReplacement); + } + + return template.transform(text); + } catch (e) { + console.error("Error in transform function:", e); + return text; + } +}; + type AttributeButtonPopoverProps = { items: T[]; onItemSelect?: (selectedItem: T) => void; @@ -59,18 +126,55 @@ const AttributeButtonPopover = ({ return String(item).toLowerCase().includes(query.toLowerCase()); }; const [sliderValue, setSliderValue] = useState(0); + useEffect(() => { setSliderValue(Number(currentValue)); }, [isOpen, currentValue]); + + const formatConfig = useMemo(() => { + try { + const configUid = getPageUidByPageTitle(CONFIG); + const attributesNode = getSubTree({ + key: "attributes", + parentUid: configUid, + }); + const attributeUid = getSubTree({ + key: attributeName, + parentUid: attributesNode.uid, + }).uid; + + return { + templateName: getSettingValueFromTree({ + key: "template", + parentUid: attributeUid, + }) || "No styling", + + customPattern: getSettingValueFromTree({ + key: "customPattern", + parentUid: attributeUid, + }), + + customReplacement: getSettingValueFromTree({ + key: "customReplacement", + parentUid: attributeUid, + }) + }; + } catch (e) { + console.error("Error getting format config:", e); + return { + templateName: "No styling", + customPattern: undefined, + customReplacement: undefined + }; + } + }, [attributeName]); - const formatDisplayText = (text: string): string => { - // TODO: for doantrang982/eng-77-decouple-display-from-output: Create formatDisplayText from configPage - // const match = text.match(/\[\[(.*?)\]\]/); - // if (match && match[1]) { - // return match[1]; - // } - return text; - }; + const formatText = useMemo(() => + (text: string) => applyFormatting({ + text, + ...formatConfig + }), + [formatConfig]); // Only show filter if we have more than 10 items const shouldFilter = filterable && items.length > 10; @@ -82,7 +186,7 @@ const AttributeButtonPopover = ({ items={items} activeItem={currentValue as T} filterable={shouldFilter} - // transformItem={(item) => formatDisplayText(String(item))} + transformItem={(item) => formatText(String(item))} onItemSelect={(s) => { updateBlock({ text: `${attributeName}:: ${s}`, @@ -471,6 +575,35 @@ const TabsPanel = ({ const [optionType, setOptionType] = useState(initialOptionType || "text"); const [min, setMin] = useState(Number(rangeNode.children[0]?.text) || 0); const [max, setMax] = useState(Number(rangeNode.children[1]?.text) || 10); + + const { initialTemplate, initialCustomPattern, initialCustomReplacement } = useMemo(() => { + const savedTemplate = getSettingValueFromTree({ + key: "template", + parentUid: attributeUid, + }) || "No styling"; + + const savedCustomPattern = getSettingValueFromTree({ + key: "customPattern", + parentUid: attributeUid, + }) || ""; + + const savedCustomReplacement = getSettingValueFromTree({ + key: "customReplacement", + parentUid: attributeUid, + }) || ""; + + return { + initialTemplate: savedTemplate, + initialCustomPattern: savedCustomPattern, + initialCustomReplacement: savedCustomReplacement + }; + }, [attributeUid]); + + const [selectedTemplate, setSelectedTemplate] = useState(initialTemplate); + const [customPattern, setCustomPattern] = useState(initialCustomPattern); + const [customReplacement, setCustomReplacement] = useState(initialCustomReplacement); + const [isValidRegex, setIsValidRegex] = useState(true); + // For a better UX replace renderBlock with a controlled list // add Edit, Delete, and Add New buttons @@ -567,16 +700,135 @@ const TabsPanel = ({ {optionType === "text" && ( -