Skip to content

[ENG-77] Add formatting option for attribute select #497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 21, 2025

Conversation

trangdoan982
Copy link
Contributor

@trangdoan982 trangdoan982 commented Apr 2, 2025

Summary by CodeRabbit

  • New Features
    • Introduced dynamic text formatting for text attributes.
    • Added multiple formatting templates, including options for uppercase, capitalization, and custom regex-based formatting.
    • Updated the settings panel with new controls for selecting a format template and entering custom patterns.
    • Enhanced error handling to ensure valid custom formats.

@trangdoan982 trangdoan982 force-pushed the eng-77/attribute-display branch from e666721 to b1fc8f3 Compare April 2, 2025 18:01
@mdroidian
Copy link
Collaborator

FYI if you post the raw loom URL, GitHub embeds it :)

EG:
https://www.loom.com/share/d10fcf5ecd394f0e8663d0c1fb78d918?sid=254c40bf-2040-4f65-9ee6-e79e4d40eaa6

Copy link
Collaborator

@mdroidian mdroidian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super cool! This looks great, unfortunately, we won't be able to allow the user to run arbitrary JavaScript as there is too much security risk. 😔 We use to do this awhile back in smartblocks but it got nixed: https://github.com/RoamJs/smartblocks/blob/main/docs/070-developer-docs.md#migrating-from-javascript-commands

Still, I do like the idea of having some presets ... so if you want to keep that in there but just use/default to regex as the custom format, I'm down with exploring that.

@trangdoan982
Copy link
Contributor Author

@mdroidian update: only allow user to either:

  • select formatting options from a drop down
    Screenshot 2025-04-07 at 00 49 20

  • or create custom format with regex:
    image

@trangdoan982 trangdoan982 requested a review from mdroidian April 7, 2025 17:53
@mdroidian
Copy link
Collaborator

@coderabbitai full review

Copy link

coderabbitai bot commented Apr 8, 2025

✅ Actions performed

Full review triggered.

Copy link

coderabbitai bot commented Apr 8, 2025

Walkthrough

The changes introduce a new text formatting feature in the attributeSelect.tsx file. A TEMPLATE_MAP constant defines various text transformation templates, and an applyFormatting function applies these transformations to input text, including custom regex patterns. The state management in the TabsPanel component is updated to accommodate user-selected formatting options, enhancing the UI with new input fields for templates and regex patterns.

Changes

File Change Summary
src/features/attributeSelect.tsx Added a TEMPLATE_MAP constant with various text transformation options, new types TemplateName and FormatParams, and an applyFormatting function. Updated the formatDisplayText function to use the new formatting logic. Enhanced the TabsPanel UI with state variables and input fields for selecting templates and custom regex patterns.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TabsPanel
    participant Formatter
    participant TemplateMap

    User->>TabsPanel: Select formatting template & enter custom regex
    TabsPanel->>Formatter: Invoke formatText(text, settings)
    Formatter->>TemplateMap: Determine transformation based on selected template
    TemplateMap-->>Formatter: Execute transformation function
    Formatter-->>TabsPanel: Return formatted text
    TabsPanel-->>User: Display updated text
Loading

Poem

I'm a rabbit, hopping through the code,
Formatting text on a winding road.
Templates and methods in a playful dance,
Regex and state give the text a chance!
With every line, my joy does grow—
Cheers to changes, now watch them glow!
🥕✨

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/features/attributeSelect.tsx (4)

40-70: The template implementations look good, but consider some improvements.

The format templates are well structured with both transformation functions and descriptions. A few suggestions:

  1. The "Remove Double Brackets" regex might be too restrictive with its start/end anchors (^$), which will only work on strings that are exactly [[text]].

  2. The "Capitalize Words" function has limitations with certain words (like "don't" becoming "Don'T").

const TEMPLATE_MAP = {
  "No styling": {
    transform: (text: string) => text,
    description: "No styling"
  },
  "Remove Double Brackets": {
-   transform: (text: string) => text.replace(/^\[\[(.*?)\]\]$/g, '$1'),
+   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 => word.charAt(0).toUpperCase() + word.slice(1)).join(' '),
+   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"
  },
  // ... rest of the code
}

74-93: Good implementation of the formatting function with error handling.

The applyFormatting function handles both standard templates and custom regex patterns with appropriate error catching. However, consider a couple of improvements:

  1. Use a stricter type for templateName parameter
  2. Provide a way to surface errors to users beyond console logs
-const applyFormatting = (
-  text: string, 
-  templateName: string, 
+const applyFormatting = (
+  text: string, 
+  templateName: TemplateName | string, 
  customPattern?: string, 
  customReplacement?: string
): 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;
  }
};

734-783: The custom format UI is comprehensive, but could use validation feedback.

The UI for custom regex formatting is well implemented with input fields and a preview. Consider adding immediate validation feedback for regex patterns rather than just logging errors to console.

You could add a state variable to track regex validity and provide visual feedback:

const [customPattern, setCustomPattern] = useState(initialCustomPattern);
const [customReplacement, setCustomReplacement] = useState(initialCustomReplacement);
+const [isValidRegex, setIsValidRegex] = useState(true);

// Then in the onChange handler:
onChange={(e) => {
  const newValue = e.target.value;
  setCustomPattern(newValue);
+  try {
+    if (newValue) new RegExp(newValue);
+    setIsValidRegex(true);
+  } catch (e) {
+    setIsValidRegex(false);
+  }
  setInputSetting({
    blockUid: attributeUid,
    key: "customPattern",
    value: newValue,
  });
}}

// Then add visual feedback:
+<input
+  className={`bp3-input font-mono text-sm w-full ${!isValidRegex ? 'bp3-intent-danger' : ''}`}
+  // ...
+/>
+{!isValidRegex && (
+  <div className="text-red-500 text-xs mt-1">
+    Invalid regular expression
+  </div>
+)}

770-782: The preview section is a helpful user experience touch.

Showing users a live preview of how their regex will transform text is a great feature. Consider making the example more dynamic or providing multiple examples that cover common use cases.

<div className="bg-gray-100 p-2 rounded text-sm">
  <div className="font-bold mb-1">Preview:</div>
  <div>
-    <span className="font-bold">Input:</span> <span className="font-mono">[[Example]]</span>
+    <span className="font-bold">Input:</span> <span className="font-mono">[[Example]]</span>
  </div>
  <div>
    <span className="font-bold">Output:</span> <span className="font-mono">
      {customPattern ? 
        applyFormatting("[[Example]]", "Custom Format", customPattern, customReplacement) :
        "[[Example]]"}
    </span>
  </div>
+  <div className="mt-2">
+    <span className="font-bold">Input:</span> <span className="font-mono">Page [[Reference]]</span>
+  </div>
+  <div>
+    <span className="font-bold">Output:</span> <span className="font-mono">
+      {customPattern ? 
+        applyFormatting("Page [[Reference]]", "Custom Format", customPattern, customReplacement) :
+        "Page [[Reference]]"}
+    </span>
+  </div>
</div>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2477fe7 and bf677a8.

📒 Files selected for processing (1)
  • src/features/attributeSelect.tsx (7 hunks)
🔇 Additional comments (5)
src/features/attributeSelect.tsx (5)

124-161: Well-implemented formatting logic with good error handling.

The formatDisplayText function properly retrieves configuration settings and applies the formatting with error handling. The organization is clear and readable.


173-173: Great use of the formatting function.

Using the new transformItem prop to apply formatting for display is a clean way to integrate the new functionality.


563-588: Good state management with useMemo for initial values.

Using useMemo to compute the initial values from settings is an efficient approach. The default values are also sensible.


686-732: Well-designed UI with helpful tooltips.

The Display Format UI has a clean layout with good user guidance through tooltips. The dropdown implementation using MenuItemSelect is appropriate.


787-797: Button placement is appropriate.

Keeping the "Find All Current Values" button as part of the formatting UI section maintains a logical flow in the interface.

Copy link
Collaborator

@mdroidian mdroidian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💥
the transformItem comment is the only blocking request

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/features/attributeSelect.tsx (2)

737-743: Consider mapping templates programmatically.

Rather than manually creating list items for each template, you could map over the TEMPLATE_MAP entries, which would be more maintainable if templates are added or removed.

-{Object.entries(TEMPLATE_MAP).map(([name, { description }]) => (
-  <li key={name}>
-    <span className="font-mono">{name}:</span>{" "}
-    {description}
-  </li>
-))}
+{Object.entries(TEMPLATE_MAP).map(([name, { description }]) => (
+  <li key={name}>
+    <span className="font-mono">{name}:</span>{" "}
+    {description}
+  </li>
+))}

134-170: 🛠️ Refactor suggestion

Consider caching formatConfig query results.

The formatConfig useMemo performs multiple database queries for each attribute. Since this function runs whenever the attributeName changes and is used in transformItem (which runs for every item in the list), it could impact performance.

Apply this refactoring to improve performance:

-  const formatConfig = useMemo(() => {
+  const formatConfig = useMemo(() => {
     try {
       const configUid = getPageUidByPageTitle(CONFIG);
-      const attributesNode = getSubTree({
-        key: "attributes",
-        parentUid: configUid,
-      });
-      const attributeUid = getSubTree({
-        key: attributeName,
-        parentUid: attributesNode.uid,
-      }).uid;
+      // Get the attributeUid once
+      const attributesNode = getSubTree({ key: "attributes", parentUid: configUid });
+      const attributeUid = getSubTree({ key: attributeName, parentUid: attributesNode.uid }).uid;
       
+      // Get all settings at once with a single query if possible
       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]);
🧹 Nitpick comments (1)
src/features/attributeSelect.tsx (1)

753-817: Consider extracting the Custom Format UI to a separate component.

The Custom Format section is quite large and could be extracted into its own component to improve code maintainability.

+const CustomFormatEditor = ({
+  customPattern,
+  setCustomPattern,
+  customReplacement,
+  setCustomReplacement,
+  isValidRegex,
+  setIsValidRegex,
+  attributeUid
+}) => {
+  return (
+    <div className="space-y-2">
+      <FormGroup label="Pattern (regex)" className="m-0">
+        <input
+          className={`bp3-input font-mono text-sm w-full ${!isValidRegex ? 'bp3-intent-danger' : ''}`}
+          placeholder="E.g., \[\[(.*?)\]\]"
+          value={customPattern}
+          onChange={(e) => {
+            const newValue = e.target.value;
+            setCustomPattern(newValue);
+            try {
+              if (newValue) new RegExp(newValue);
+              setIsValidRegex(true);
+            } catch (e) {
+              setIsValidRegex(false);
+            }
+            setInputSetting({
+              blockUid: attributeUid,
+              key: "customPattern",
+              value: newValue,
+            });
+          }}
+        />
+        {!isValidRegex && (
+          <div className="text-red-500 text-xs mt-1">
+            Invalid regular expression
+          </div>
+        )}
+      </FormGroup>
+      
+      <FormGroup label="Replacement" className="m-0">
+        <input
+          className="bp3-input font-mono text-sm w-full"
+          placeholder="E.g., $1"
+          value={customReplacement}
+          onChange={(e) => {
+            const newValue = e.target.value;
+            setCustomReplacement(newValue);
+            setInputSetting({
+              blockUid: attributeUid,
+              key: "customReplacement",
+              value: newValue,
+            });
+          }}
+        />
+      </FormGroup>
+      
+      <div className="bg-gray-100 p-2 rounded text-sm">
+        <div className="font-bold mb-1">Preview:</div>
+        <div>
+          <span className="font-bold">Input:</span> <span className="font-mono">[[Example]]</span>
+        </div>
+        <div>
+          <span className="font-bold">Output:</span> <span className="font-mono">
+            {customPattern ? 
+              applyFormatting({
+                text: "[[Example]]",
+                templateName: "Custom Format",
+                customPattern,
+                customReplacement
+              }) :
+              "[[Example]]"}
+          </span>
+        </div>
+      </div>
+    </div>
+  );
+};

// Then in the TabsPanel component:
{selectedTemplate === "Custom Format" && (
  <CustomFormatEditor
    customPattern={customPattern}
    setCustomPattern={setCustomPattern}
    customReplacement={customReplacement}
    setCustomReplacement={setCustomReplacement}
    isValidRegex={isValidRegex}
    setIsValidRegex={setIsValidRegex}
    attributeUid={attributeUid}
  />
)}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bf677a8 and 9d5af5c.

📒 Files selected for processing (1)
  • src/features/attributeSelect.tsx (7 hunks)
🔇 Additional comments (9)
src/features/attributeSelect.tsx (9)

40-73: Well-structured template system for text formatting.

The TEMPLATE_MAP implementation provides a clean, maintainable way to define text transformations. The inclusion of transform functions with descriptions makes the code self-documenting.


75-82: Good type definitions for template types and formatting parameters.

Using TypeScript types effectively ensures type safety throughout the application. The explicit type definitions make the code more maintainable and help prevent errors.


84-103: Well-implemented formatting function with proper error handling.

The applyFormatting function has good error handling for both general exceptions and specific template scenarios. The function is designed to be resilient and never crash, with helpful console error logging.


172-177: Clean implementation of formatText with memoization.

Good use of useMemo to avoid unnecessary recalculations of the formatted text when the dependencies haven't changed.


704-820: Comprehensive UI for formatting options with good user guidance.

The UI implementation for the formatting options is well-designed with:

  1. Informative tooltips explaining the feature
  2. Clear organization of options
  3. Live preview for custom regex patterns
  4. Validation feedback for regex errors

This provides an excellent user experience for both basic and advanced users.


579-606: Good state initialization from persisted settings.

The component correctly initializes state from saved settings and provides defaults when needed. The use of useMemo to compute initial values just once is efficient.


799-816: Nice touch with the live preview.

The preview section is an excellent UX feature that helps users understand how their regex pattern will transform text. This is particularly valuable for those who might not be familiar with regex syntax.


189-189: Improved text transformation in list items.

The change from the previous implementation to using the formatText function ensures consistent formatting across the application.


755-779:

❓ Verification inconclusive

Ensure regex validation is consistent with usage.

The validation logic checks if a pattern can be used to create a RegExp object, but doesn't verify that it would work as expected with the replacement pattern. Consider adding more comprehensive validation.

For regex patterns involving capture groups, ensure users understand how to reference them in the replacement string. For example, if a user creates a pattern with capture groups (()), they would need to use $1, $2, etc. in the replacement string to reference those groups.


🌐 Web query:

What are common mistakes when using JavaScript regex replacements with capture groups?

💡 Result:

Common mistakes when using JavaScript regex replacements with capture groups often stem from syntax errors, misnumbered references, and incorrect assumptions about regex behavior. Here are key issues to avoid:

1. Invalid Capture Group Names

Naming groups with non-identifier characters (e.g., numbers or symbols) triggers errors. Named groups must use valid JavaScript identifiers:

/(?<1>\d+)/; // SyntaxError: Invalid capture group name [1]

Valid named group:

/(?<group1>\d+)/; // Correct [1][8]

2. Incorrect Group Syntax

Using (? without a valid group specifier (e.g., ?:, ?=, or ?<) causes errors:

/Hello(?|!)/; // SyntaxError: invalid regexp group [2]

Fix by escaping characters or using valid syntax:

/Hello(\?|!)/; // Correct [2]

3. Misnumbered Backreferences

Referencing nonexistent groups (e.g., $4 when only 3 groups exist) fails silently or causes unexpected results. Example from a failed replacement:

// Intended to swap "one two three" to "three two one"
let fixRegex = /(\w+)\s(\w)+\s(\w+)/; // Incorrectly captures middle word
let replaceText = "$3 $2 $1"; // May reference undefined groups [9]

Correct approach with proper grouping:

let fixRegex = /(\w+) (\w+) (\w+)/;
let replaceText = "$3 $2 $1"; // Outputs "three two one" [9][3]

4. Unanchored Patterns Causing Partial Matches

Regex patterns that don’t anchor to the start (^) and end ($) of the string may unexpectedly match substrings:

/(\d+)\s\1\s\1/.test("42 42 42 100"); // Returns true unintentionally [4]

Anchored solution:

/^(\d+)\s\1\s\1$/.test("42 42 42"); // Matches exact string only [4]

5. Incorrect Replacement Syntax

Using $1 vs. \1 depends on the context. In JavaScript’s String.replace(), use $1, but in some environments (e.g., Unix sed), use \1 [6]:

// JavaScript
"test".replace(/(st)/, "$1"); // Correct [6]

6. Named Group Access Errors

Failing to check for null matches before accessing .groups causes runtime errors:

const match = regex.exec(input);
// Risky:
const { group } = match.groups; // TypeError if match is null [10]
// Safe:
const group = match?.groups?.group; [10]

7. Dynamic Regex Construction Pitfalls

When building regex dynamically, ensure group names or indices are correctly interpolated:

// Incorrect if tokenTypes keys are invalid identifiers
const tokenPattern = new RegExp(`(?<${name}>...)`); // May fail [1]
// Fix by validating keys:
const tokenTypes = { validName: /.../ }; // Correct [1]

By avoiding these errors and validating regex patterns with tools like RegExp Tester, you can effectively leverage capture groups in replacements.

Citations:


Action Required: Enhance Regex Validation Consistency and Guidance on Capture Groups

The current validation logic verifies that the regex pattern is syntactically correct (via instantiation of a RegExp object), but it does not ensure that the regex behaves as intended for replacement operations—especially when capture groups are involved. For instance, if a user creates a pattern with capture groups, they must reference them correctly (using $1, $2, etc.) in any replacement string. This gap can lead to common issues such as misnumbered backreferences or incorrect assumptions about the regex behavior.

Please consider one or more of the following actions:

  • Extend Validation: Implement additional checks to analyze the pattern for capture groups and, if applicable, ensure that corresponding references in replacement strings are valid.
  • User Guidance: Provide inline documentation or error messaging that explicitly informs users about the required replacement syntax for capture groups, highlighting common pitfalls (e.g., referencing non-existent groups or using improper syntax).
  • Reference Resources: Incorporate best practices from sources that detail common mistakes with JavaScript regex replacements, as discovered in community examples.

@trangdoan982 trangdoan982 changed the title Add formatting option for attribute select [ENG-77] Add formatting option for attribute select Apr 14, 2025
@trangdoan982 trangdoan982 requested a review from mdroidian April 14, 2025 19:42
Copy link
Collaborator

@mdroidian mdroidian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@mdroidian mdroidian merged commit f202e8c into RoamJS:main Apr 21, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants