Skip to content

Conversation

@trangdoan982
Copy link
Collaborator

@trangdoan982 trangdoan982 commented Oct 27, 2025

https://www.loom.com/share/11478ad987614ae4bdee0c0c61504fa8

Old PR comments can be found here: #490

Summary by CodeRabbit

  • New Features
    • Added a boolean "key image" option to node-type settings.
    • Canvas nodes can preload and display a key image from linked files.
    • Node dimensions are now calculated dynamically from title, type and image.
    • Nodes auto-resize (and respect manual resizing) to better fit text and images.

@linear
Copy link

linear bot commented Oct 27, 2025

ENG-869 Key figure

@supabase
Copy link

supabase bot commented Oct 27, 2025

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@trangdoan982
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

📝 Walkthrough

Walkthrough

Adds optional key-image support for discourse node types: new boolean field, image extraction/loading utilities, automatic size calculation, and canvas shape updates to render and resize nodes with images.

Changes

Cohort / File(s) Change Summary
Node type configuration
apps/obsidian/src/components/NodeTypeSettings.tsx, apps/obsidian/src/types.ts
Adds boolean field type support and a BooleanField component; introduces keyImage boolean field in node-type configs and extends DiscourseNode type with optional keyImage. Adjusts field rendering/validation to accept string
Canvas node creation
apps/obsidian/src/components/canvas/ExistingNodeSearch.tsx
Resolves nodeTypeId from file frontmatter, optionally preloads first image, computes node w/h with calcDiscourseNodeSize, and creates the node with imageSrc, w, h, and nodeTypeId. Adds try/catch and updates dependencies.
Canvas shape rendering & behavior
apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx, apps/obsidian/src/components/canvas/shapes/discourseNodeShapeUtils.ts
Adds imageSrc prop to shape; renders image when present; fetches/caches first image from linked file; recomputes and updates dimensions when image changes; enables manual resizing with aspect unlocked and uses resizeBox. Adds getFirstImageSrcForFile helper.
Constants
apps/obsidian/src/components/canvas/shapes/nodeConstants.ts
New module exporting layout, typography, padding, and image sizing constants used by Discourse node rendering.
Sizing & image utilities
apps/obsidian/src/utils/calcDiscourseNodeSize.ts, apps/obsidian/src/utils/loadImage.ts, apps/obsidian/src/utils/measureNodeText.ts
New utilities: calcDiscourseNodeSize (measures text, optionally loads image to compute final w/h), loadImage (loads image with timeout, returns natural size), and measureNodeText (DOM-based text measurement constrained by min/max widths).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ExistingNodeSearch
    participant Utils as Utilities
    participant DiscourseNodeShape

    User->>ExistingNodeSearch: select file to create node
    ExistingNodeSearch->>ExistingNodeSearch: read frontmatter -> fmNodeTypeId
    alt fmNodeType enables keyImage
        ExistingNodeSearch->>Utils: getFirstImageSrcForFile(file)
        Utils-->>ExistingNodeSearch: imageSrc or null
    end
    ExistingNodeSearch->>Utils: calcDiscourseNodeSize(title, nodeTypeId, imageSrc)
    Utils->>Utils: measureNodeText(...)
    alt imageSrc present
        Utils->>Utils: loadImage(imageSrc) -> image dims
    end
    Utils-->>ExistingNodeSearch: { w, h }
    ExistingNodeSearch->>DiscourseNodeShape: create node with imageSrc, w, h, nodeTypeId
    DiscourseNodeShape->>DiscourseNodeShape: render title (+ image if imageSrc)
    DiscourseNodeShape->>DiscourseNodeShape: onResize -> resizeBox -> update w/h
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay attention to DOM insertion/removal and CSS parity in measureNodeText.ts.
  • Verify image loading edge cases and timeout handling in loadImage.ts.
  • Confirm fallback behavior and async race conditions in calcDiscourseNodeSize.ts.
  • Review image extraction/URL normalization and vault resource resolution in getFirstImageSrcForFile.
  • Check integration points in ExistingNodeSearch.tsx and DiscourseNodeShape.tsx for correct props, dependency arrays, and selection handling.

Possibly related PRs

Pre-merge checks

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The PR title "[ENG-869] Key figure" appears to reference the keyImage feature being introduced in this changeset, which involves adding boolean field configuration support, image display capability in discourse nodes, and associated utility functions for image handling and sizing. However, the terminology used is imprecise and vague—"key figure" is not a standard technical term, and while it may be intended to represent "key image," the wording obscures the actual nature of the change. A teammate scanning commit history would likely find this title ambiguous regarding what was specifically implemented, as the term could refer to many different things without additional context.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/obsidian/src/components/NodeTypeSettings.tsx (1)

431-455: Boolean-aware validation: avoid string-only .trim() and checks.

validateNodeType assumes strings; future required boolean fields will break. Make it type-aware.

-    Object.entries(FIELD_CONFIGS).forEach(([key, config]) => {
+    Object.entries(FIELD_CONFIGS).forEach(([key, config]) => {
       const field = key as EditableFieldKey;
-      const value = nodeType[field] as string;
+      const raw = nodeType[field];
+      const value = typeof raw === "string" ? raw : "";
 
-      if (config.required && !value?.trim()) {
+      if (config.required) {
+        if (config.type === "boolean") {
+          if (raw !== true) {
+            newErrors[field] = `${config.label} is required`;
+            isValid = false;
+            return;
+          }
+        } else if (!value?.trim()) {
+          newErrors[field] = `${config.label} is required`;
+          isValid = false;
+          return;
+        }
+      }
 
-      if (config.validate && value) {
+      if (config.validate && typeof raw === "string" && value) {
         const { isValid: fieldValid, error } = config.validate(
           value,
           nodeType,
           nodeTypes,
         );
🧹 Nitpick comments (13)
apps/obsidian/src/components/NodeTypeSettings.tsx (4)

35-35: Type now includes "boolean"; tighten validation typing to avoid future footguns.

validate in BaseFieldConfig takes string, and downstream validators/required checks assume strings. It works because boolean fields skip validation, but this is fragile if a boolean ever becomes required. Consider a discriminated union for field configs to encode type at the type level and prevent string-only checks from touching booleans.


131-144: Checkbox a11y: associate with label/description.

Wire an accessible name so screen readers announce what this checkbox controls.

Apply:

-const BooleanField = ({
-  value,
-  onChange,
-}: {
-  value: boolean;
-  onChange: (value: boolean) => void;
-}) => (
-  <input
-    type="checkbox"
-    checked={!!value}
-    onChange={(e) => onChange((e.target as HTMLInputElement).checked)}
-  />
-);
+const BooleanField = ({
+  value,
+  onChange,
+  ariaLabel,
+}: {
+  value: boolean;
+  onChange: (value: boolean) => void;
+  ariaLabel?: string;
+}) => (
+  <input
+    type="checkbox"
+    checked={!!value}
+    aria-label={ariaLabel}
+    onChange={(e) => onChange((e.target as HTMLInputElement).checked)}
+  />
+);

322-329: Validation gating by typeof is fine; also clear stale errors on boolean edits.

Since booleans skip validateField, consider clearing any prior error for this field on boolean change to avoid stale error states after type switches.

-  const updatedNodeType = { ...editingNodeType, [field]: value };
+  const updatedNodeType = { ...editingNodeType, [field]: value };
   if (typeof value === "string") {
     validateField(field, value, updatedNodeType);
   }
+  if (typeof value === "boolean") {
+    setErrors((prev) => {
+      const { [field]: _, ...rest } = prev;
+      return rest;
+    });
+  }

461-488: Narrow the onChange types per control to avoid bivariant function pitfalls.

Pass field-type-specific handlers instead of a union-typed handleChange for stronger type safety.

-    const value = editingNodeType[fieldConfig.key] as string | boolean;
-    const error = errors[fieldConfig.key];
-    const handleChange = (newValue: string | boolean) =>
-      handleNodeTypeChange(fieldConfig.key, newValue);
+    const value = editingNodeType[fieldConfig.key] as string | boolean;
+    const error = errors[fieldConfig.key];
     ...
-          <TemplateField
-            value={value as string}
-            error={error}
-            onChange={handleChange}
+          <TemplateField
+            value={value as string}
+            error={error}
+            onChange={(v: string) => handleNodeTypeChange(fieldConfig.key, v)}
             templateConfig={templateConfig}
             templateFiles={templateFiles}
           />
     ) : fieldConfig.type === "color" ? (
-          <ColorField
-            value={value as string}
-            error={error}
-            onChange={handleChange}
-          />
+          <ColorField
+            value={value as string}
+            error={error}
+            onChange={(v: string) => handleNodeTypeChange(fieldConfig.key, v)}
+          />
     ) : fieldConfig.type === "boolean" ? (
-          <BooleanField value={value as boolean} onChange={handleChange} />
+          <BooleanField
+            value={value as boolean}
+            ariaLabel={fieldConfig.label}
+            onChange={(v: boolean) => handleNodeTypeChange(fieldConfig.key, v)}
+          />
     ) : (
           <TextField
             fieldConfig={fieldConfig}
             value={value as string}
             error={error}
-            onChange={handleChange}
+            onChange={(v: string) => handleNodeTypeChange(fieldConfig.key, v)}
             nodeType={editingNodeType}
           />
apps/obsidian/src/utils/loadImage.ts (1)

11-39: Harden image loader: cleanup handlers, SSR guard, optional CORS hint.

Prevents leaks and improves robustness in tests/SSR.

 export const loadImage = (
   url: string,
 ): Promise<{ width: number; height: number }> => {
-  return new Promise((resolve, reject) => {
+  return new Promise((resolve, reject) => {
+    if (typeof Image === "undefined") {
+      reject(new Error("Image API not available in this environment"));
+      return;
+    }
     const img = new Image();
     let resolved = false;
 
-    const timeoutId = setTimeout(() => {
+    // Allow natural size without tainting canvas if later used
+    img.crossOrigin = "anonymous";
+
+    const timeoutId = setTimeout(() => {
       if (!resolved) {
         resolved = true;
-        reject(new Error("Failed to load image: timeout"));
+        // Best-effort cleanup
+        img.onload = null;
+        img.onerror = null;
+        img.src = "";
+        reject(new Error("Failed to load image: timeout"));
       }
     }, 10000);
 
-    img.onload = () => {
+    const cleanup = () => {
+      img.onload = null;
+      img.onerror = null;
+      clearTimeout(timeoutId);
+    };
+
+    img.onload = () => {
       if (!resolved) {
         resolved = true;
-        clearTimeout(timeoutId);
+        cleanup();
         resolve({ width: img.naturalWidth, height: img.naturalHeight });
       }
     };
 
-    img.onerror = () => {
+    img.onerror = () => {
       if (!resolved) {
         resolved = true;
-        clearTimeout(timeoutId);
+        cleanup();
         reject(new Error("Failed to load image"));
       }
     };
 
     img.src = url;
   });
 };
apps/obsidian/src/utils/measureNodeText.ts (3)

25-45: Keep measurement in sync with component styles.

Inline styles risk drifting from the real Tailwind classes. Prefer applying the same className strings to the temp elements so future style tweaks automatically reflect here.

Example:

container.className = "box-border flex flex-col items-start justify-start rounded-md border-2 p-2";
titleEl.className = "m-1 text-base font-semibold";
subtitleEl.className = "m-0 text-sm";

Then only set min/max width inline.


27-45: Minimize layout side-effects during measurement.

Move it far off-screen to mitigate any accidental hit-testing/overlap in edge cases.

   container.style.setProperty("position", "absolute");
   container.style.setProperty("visibility", "hidden");
   container.style.setProperty("pointer-events", "none");
+  container.style.setProperty("left", "-10000px");
+  container.style.setProperty("top", "-10000px");

69-73: Return integer dimensions to avoid subpixel jitter.

Rounding prevents tiny pixel differences causing layout churn downstream.

-  return {
-    w: rect.width,
-    h: rect.height,
-  };
+  return {
+    w: Math.ceil(rect.width),
+    h: Math.ceil(rect.height),
+  };
apps/obsidian/src/components/canvas/shapes/discourseNodeShapeUtils.ts (2)

41-61: Normalize target before external/internal resolution.

Angle-bracketed markdown targets like https://... won’t pass the external test before normalization. Normalize first, then branch.

-      if (match[2]) {
-        const target = match[2].trim();
-
-        // External URL - return directly
-        if (/^https?:\/\//i.test(target)) {
-          return target;
-        }
+      if (match[2]) {
+        const normalized = normalizeLinkTarget(match[2].trim());
+        const target = normalized ?? match[2].trim();
+
+        // External URL - return directly
+        if (/^https?:\/\//i.test(target)) {
+          return target;
+        }
 
-        // Internal path - resolve to vault file
-        const normalized = normalizeLinkTarget(target);
-        const tfile = app.metadataCache.getFirstLinkpathDest(
-          normalized ?? target,
+        // Internal path - resolve to vault file
+        const tfile = app.metadataCache.getFirstLinkpathDest(
+          target,
           file.path,
         );

26-80: Consider metadataCache embeds for performance and accuracy.

Parsing the whole file may match images inside code blocks. metadataCache.getFileCache(file)?.embeds (and .links) already provide resolved targets; using them avoids full reads and regex pitfalls.

apps/obsidian/src/utils/calcDiscourseNodeSize.ts (1)

70-74: Consider adding type safety for the error.

The catch block logs the error but uses a generic type. For better debugging, consider typing the error parameter.

-  } catch (error) {
+  } catch (error: unknown) {
     // If image fails to load, fall back to text-only dimensions
     console.warn("calcDiscourseNodeSize: failed to load image", error);
     return { w, h: textHeight };
apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx (1)

213-226: Clarify the 1-pixel threshold rationale.

The 1px threshold (lines 214-215) prevents unnecessary updates when dimensions are nearly identical. Combined with excluding w and h from the dependencies (line 238-240), this avoids fighting manual resizing.

Consider adding a comment near the threshold check explaining that this tolerance allows minor calculation variations while respecting user-initiated resizes.

   const { w, h } = await calcDiscourseNodeSize({
     title: linkedFile.basename,
     nodeTypeId: shape.props.nodeTypeId,
     imageSrc: currentImageSrc,
     plugin,
   });

+  // Only update dimensions if they differ significantly (>1px)
+  // This avoids fighting manual resizing and accounts for minor calculation variances
   if (
     Math.abs((shape.props.w || 0) - w) > 1 ||
     Math.abs((shape.props.h || 0) - h) > 1
apps/obsidian/src/components/canvas/shapes/nodeConstants.ts (1)

1-32: Consider grouping related constants.

The constants are well-named and documented. For better organization, consider grouping them by category (dimensions, spacing, typography, images) with section comments.

 /**
  * Constants for Discourse Node styling and sizing.
  * These values match the Tailwind classes used in DiscourseNodeShape component.
  */

+// Dimension constraints
 export const DEFAULT_NODE_WIDTH = 200;
 export const MIN_NODE_WIDTH = 160;
 export const MAX_NODE_WIDTH = 400;

-// Padding: p-2 = 0.5rem = 8px on each side = 16px total vertical
+// Spacing
+// Padding and border: p-2 (8px each side) + border-2 (2px each side) = 20px total
 export const BASE_PADDING = 20;
+export const IMAGE_GAP = 4;

-// Font sizes matching Tailwind classes
+// Typography
 export const TITLE_FONT_SIZE = 16; // text-base
 export const SUBTITLE_FONT_SIZE = 14; // text-sm
-
-// Line height for text rendering
 export const LINE_HEIGHT = 1.5;
-
-// Font family - use system font stack similar to Tailwind
 export const FONT_FAMILY =
   'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';

-// Maximum height for key images
+// Image constraints
 export const MAX_IMAGE_HEIGHT = 250;
-
-// Gap between image and text
-export const IMAGE_GAP = 4;
-
-// Base height for nodes without images (estimated)
 export const BASE_HEIGHT_NO_IMAGE = 100;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d803704 and 7fccedd.

📒 Files selected for processing (9)
  • apps/obsidian/src/components/NodeTypeSettings.tsx (5 hunks)
  • apps/obsidian/src/components/canvas/ExistingNodeSearch.tsx (3 hunks)
  • apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx (7 hunks)
  • apps/obsidian/src/components/canvas/shapes/discourseNodeShapeUtils.ts (1 hunks)
  • apps/obsidian/src/components/canvas/shapes/nodeConstants.ts (1 hunks)
  • apps/obsidian/src/types.ts (1 hunks)
  • apps/obsidian/src/utils/calcDiscourseNodeSize.ts (1 hunks)
  • apps/obsidian/src/utils/loadImage.ts (1 hunks)
  • apps/obsidian/src/utils/measureNodeText.ts (1 hunks)
🔇 Additional comments (7)
apps/obsidian/src/types.ts (1)

12-12: Verified: keyImage field addition is correct and safe.

All usages employ optional chaining (nodeType?.keyImage) and falsy checks (!nodeType?.keyImage), which correctly treat missing/undefined as false. Settings merge with Object.assign handles missing fields for backward-compatible loading of existing nodeTypes. NodeTypeSettings config marks the field as non-required boolean with no string mapping.

apps/obsidian/src/components/NodeTypeSettings.tsx (1)

119-126: keyImage field implementation is correct—no issues found.

Verification confirms:

  • Field correctly defined as type: "boolean", required: false
  • New node types default to undefined (equivalent to false), as existing DEFAULT_NODE_TYPES don't include it
  • No migration needed; the optional field is safe for existing settings
  • BooleanField checkbox always returns actual boolean, never a string
  • All usage sites safely check with nodeType?.keyImage, handling undefined correctly
  • No string-to-boolean conversion risk exists in the codebase
apps/obsidian/src/components/canvas/ExistingNodeSearch.tsx (2)

63-74: Good defensive programming for image preloading.

The try-catch wrapper around image preloading with a clear warning message is a good practice. This ensures that node creation doesn't fail even if the image loading encounters issues.


77-82: Code handles empty string nodeTypeId correctly—no changes needed.

The getNodeTypeById function uses strict equality comparison (node.id === nodeTypeId) to find matching node types. An empty string "" will only match if a node type has an empty ID, which is not a practical scenario. In practice, the function returns undefined, which is safely handled downstream in calcDiscourseNodeSize via optional chaining (nodeType?.name) and a default fallback to "". This pattern is consistent across all call sites in the codebase and works as intended.

apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx (3)

66-74: LGTM: Resize handlers correctly implemented.

The resize implementation correctly uses resizeBox from tldraw and unlocks the aspect ratio to allow flexible resizing. This aligns well with the dynamic sizing behavior based on content.


262-270: Good image rendering implementation.

The image element uses appropriate attributes:

  • lazy loading for performance
  • async decoding to prevent blocking
  • draggable="false" for better UX
  • w-full for responsive sizing
  • object-cover for aspect ratio handling

178-204: The code approach is sound and correctly uses the local variable.

The verification confirms the review comment is accurate. The code maintains currentImageSrc as a local variable that's updated before calcDiscourseNodeSize is called, avoiding any issues with the non-awaited updateShape calls. Since currentImageSrc is used for size calculation (not shape.props.imageSrc), the async window is not problematic here.

The suggestion to add a clarifying comment explaining why currentImageSrc is maintained separately is a reasonable best-practice recommendation for code clarity, but the implementation is correct as-is.

@trangdoan982
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@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 (5)
apps/obsidian/src/utils/loadImage.ts (1)

8-10: Add explicit return type annotation.

The coding guidelines specify using explicit return types for functions. While TypeScript correctly infers the return type, adding it explicitly improves code clarity and enforces the contract.

Apply this diff:

-export const loadImage = (
-  url: string,
-): Promise<{ width: number; height: number }> => {
+export const loadImage = (url: string): Promise<{ width: number; height: number }> => {
apps/obsidian/src/components/canvas/shapes/discourseNodeShapeUtils.ts (1)

22-25: Add explicit return type annotation.

Per the coding guidelines, functions should have explicit return types for better clarity and type safety.

Apply this diff:

-export const getFirstImageSrcForFile = async (
-  app: App,
-  file: TFile,
-): Promise<string | null> => {
+export const getFirstImageSrcForFile = async (app: App, file: TFile): Promise<string | null> => {
apps/obsidian/src/utils/measureNodeText.ts (1)

18-24: Add explicit return type annotation.

Following the coding guidelines, add an explicit return type to the function signature for improved clarity.

Apply this diff:

-export const measureNodeText = ({
-  title,
-  subtitle,
-}: {
-  title: string;
-  subtitle: string;
-}): { w: number; h: number } => {
+export const measureNodeText = ({ title, subtitle }: { title: string; subtitle: string }): { w: number; h: number } => {
apps/obsidian/src/utils/calcDiscourseNodeSize.ts (1)

25-30: Add explicit return type annotation.

Per the coding guidelines, add an explicit return type to improve code clarity.

Apply this diff:

-export const calcDiscourseNodeSize = async ({
-  title,
-  nodeTypeId,
-  imageSrc,
-  plugin,
-}: CalcNodeSizeParams): Promise<{ w: number; h: number }> => {
+export const calcDiscourseNodeSize = async ({ title, nodeTypeId, imageSrc, plugin }: CalcNodeSizeParams): Promise<{ w: number; h: number }> => {
apps/obsidian/src/components/canvas/shapes/nodeConstants.ts (1)

30-31: Consider deriving from component constants or documenting the rationale.

The "estimated" comment suggests this value is somewhat arbitrary. For better maintainability, either calculate it from existing constants (BASE_PADDING, font sizes, line heights) or document why 100px was specifically chosen.

For example, if you want to derive it:

-// Base height for nodes without images (estimated)
-export const BASE_HEIGHT_NO_IMAGE = 100;
+// Base height for nodes without images
+// Calculated as: vertical padding + title + subtitle + gaps
+export const BASE_HEIGHT_NO_IMAGE = 
+  BASE_PADDING * 2 + // top and bottom padding
+  TITLE_FONT_SIZE * LINE_HEIGHT + 
+  SUBTITLE_FONT_SIZE * LINE_HEIGHT + 
+  8; // additional spacing between elements

Or simply document it better:

-// Base height for nodes without images (estimated)
+// Base height for nodes without images - chosen to accommodate title and subtitle with comfortable spacing
 export const BASE_HEIGHT_NO_IMAGE = 100;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d803704 and a1a6798.

📒 Files selected for processing (9)
  • apps/obsidian/src/components/NodeTypeSettings.tsx (5 hunks)
  • apps/obsidian/src/components/canvas/ExistingNodeSearch.tsx (3 hunks)
  • apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx (7 hunks)
  • apps/obsidian/src/components/canvas/shapes/discourseNodeShapeUtils.ts (1 hunks)
  • apps/obsidian/src/components/canvas/shapes/nodeConstants.ts (1 hunks)
  • apps/obsidian/src/types.ts (1 hunks)
  • apps/obsidian/src/utils/calcDiscourseNodeSize.ts (1 hunks)
  • apps/obsidian/src/utils/loadImage.ts (1 hunks)
  • apps/obsidian/src/utils/measureNodeText.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/main.mdc)

**/*.{ts,tsx}: Prefer type over interface
Use explicit return types for functions
Avoid any types when possible

Files:

  • apps/obsidian/src/utils/loadImage.ts
  • apps/obsidian/src/components/canvas/ExistingNodeSearch.tsx
  • apps/obsidian/src/utils/calcDiscourseNodeSize.ts
  • apps/obsidian/src/components/canvas/shapes/nodeConstants.ts
  • apps/obsidian/src/components/NodeTypeSettings.tsx
  • apps/obsidian/src/utils/measureNodeText.ts
  • apps/obsidian/src/types.ts
  • apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx
  • apps/obsidian/src/components/canvas/shapes/discourseNodeShapeUtils.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/main.mdc)

**/*.{ts,tsx,js,jsx}: Prefer arrow functions over regular function declarations
Use named parameters (object destructuring) when a function has more than 2 parameters
Use Prettier with the project's configuration
Maintain consistent naming conventions: PascalCase for components and types
Maintain consistent naming conventions: camelCase for variables and functions
Maintain consistent naming conventions: UPPERCASE for constants

Files:

  • apps/obsidian/src/utils/loadImage.ts
  • apps/obsidian/src/components/canvas/ExistingNodeSearch.tsx
  • apps/obsidian/src/utils/calcDiscourseNodeSize.ts
  • apps/obsidian/src/components/canvas/shapes/nodeConstants.ts
  • apps/obsidian/src/components/NodeTypeSettings.tsx
  • apps/obsidian/src/utils/measureNodeText.ts
  • apps/obsidian/src/types.ts
  • apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx
  • apps/obsidian/src/components/canvas/shapes/discourseNodeShapeUtils.ts
apps/obsidian/**

📄 CodeRabbit inference engine (.cursor/rules/obsidian.mdc)

Use the Obsidian style guide from help.obsidian.md/style-guide and docs.obsidian.md/Developer+policies for all code in the Obsidian plugin

Files:

  • apps/obsidian/src/utils/loadImage.ts
  • apps/obsidian/src/components/canvas/ExistingNodeSearch.tsx
  • apps/obsidian/src/utils/calcDiscourseNodeSize.ts
  • apps/obsidian/src/components/canvas/shapes/nodeConstants.ts
  • apps/obsidian/src/components/NodeTypeSettings.tsx
  • apps/obsidian/src/utils/measureNodeText.ts
  • apps/obsidian/src/types.ts
  • apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx
  • apps/obsidian/src/components/canvas/shapes/discourseNodeShapeUtils.ts
🔇 Additional comments (19)
apps/obsidian/src/types.ts (1)

12-12: LGTM!

The optional keyImage field is a clean, backward-compatible addition that aligns with the PR's objective to support key images for discourse node types.

apps/obsidian/src/utils/loadImage.ts (1)

11-40: LGTM!

The promise implementation correctly handles:

  • Single-resolution semantics via the resolved flag
  • 10-second timeout for stuck image loads
  • Both success and error paths
  • Cleanup of the timeout on resolution
apps/obsidian/src/components/canvas/shapes/discourseNodeShapeUtils.ts (1)

29-81: LGTM!

The implementation correctly:

  • Extracts image references from both Markdown and wiki-style embeds
  • Normalizes link targets by handling titles, angle brackets, aliases, and fragments
  • Distinguishes between external URLs and internal vault paths
  • Validates image file extensions case-insensitively
  • Includes error handling with appropriate logging
apps/obsidian/src/utils/measureNodeText.ts (1)

25-73: LGTM!

The DOM-based measurement approach correctly:

  • Creates a hidden, non-interactive container
  • Matches the component's actual styling (padding, borders, flex layout)
  • Applies proper width constraints from constants
  • Measures using getBoundingClientRect()
  • Cleans up the temporary DOM element after measurement
apps/obsidian/src/components/canvas/ExistingNodeSearch.tsx (3)

58-74: LGTM!

The image preloading logic correctly:

  • Determines node type from frontmatter
  • Only preloads when keyImage is enabled for the node type
  • Handles errors gracefully with console warnings
  • Maintains resilience by not blocking shape creation on image load failure

76-82: LGTM!

The dynamic size calculation using calcDiscourseNodeSize replaces the previous hardcoded dimensions (200×100) with accurate measurements based on actual content and optional images.


106-106: No issues found. The dependency array change is correct and safe.

The handleSelect callback accesses both plugin.app (lines 54, 58, 66) and plugin.settings (line 71). Including plugin in the dependency array instead of just plugin.app properly tracks all accessed properties. Since plugin is a stable prop reference, there are no stale closure or missed re-render risks. This approach is idiomatic React—including the parent object when accessing its properties.

apps/obsidian/src/utils/calcDiscourseNodeSize.ts (2)

70-74: LGTM!

The error handling correctly falls back to text-only dimensions when image loading fails, ensuring resilience and preventing shape creation failures due to image issues.


51-64: Correct the padding value in your verification note; p-2 is 8px, not 16px as claimed.

The review comment contains an inaccurate claim: Tailwind's p-2 class equals 0.5rem or 8px by default, not 16px. The codebase confirms this—no custom spacing theme is configured in either apps/obsidian/tailwind.config.ts or the shared packages/tailwind-config/tailwind.config.ts.

However, the core concern about padding mismatch is valid: BASE_PADDING is 20px while the container uses p-2 (8px). This 20px vs 8px discrepancy could affect the effectiveWidth calculation, which influences image dimension constraints. The calculation assumes BASE_PADDING represents usable horizontal space, but the container's actual internal padding is only 8px per side.

Verify whether this mismatch causes layout issues in practice—specifically, whether images with w-full render correctly when constrained by the effectiveWidth calculation.

apps/obsidian/src/components/canvas/shapes/DiscourseNodeShape.tsx (4)

66-74: LGTM!

The resize implementation correctly:

  • Unlocks aspect ratio to allow flexible resizing
  • Enables manual resizing via canResize
  • Uses tldraw's resizeBox utility for proper resize handling

178-231: Consider potential conflicts between manual resizing and auto-sizing.

The useEffect recalculates and updates node dimensions when didImageChange is true (lines 209-230). However, if a user manually resizes the node and then the image changes (e.g., by updating the file content), the manual resize will be overridden.

While the comment on line 243 mentions avoiding "fighting manual resizing," the current implementation still updates dimensions when the image changes. Consider whether this behavior is intentional or if you should preserve manual size adjustments after initial image load.

The 1px tolerance check (lines 218-219) helps avoid unnecessary updates, but verify that this behavior aligns with the expected user experience when:

  1. A user manually resizes a node
  2. The underlying file's image changes

If manual resizes should be preserved, consider tracking whether the user has manually adjusted the size and skip auto-sizing in that case.


243-256: LGTM!

The dependency array correctly includes all external values used within the effect, and the eslint-disable comment appropriately documents why dimensions (shape.props.w and shape.props.h) are excluded to avoid fighting manual resizing.


267-277: LGTM!

The conditional image rendering correctly:

  • Only renders when imageSrc is present
  • Uses lazy loading and async decoding for performance
  • Disables dragging to prevent interference with canvas interactions
  • Uses object-contain to preserve aspect ratio within the container
apps/obsidian/src/components/NodeTypeSettings.tsx (4)

119-126: LGTM!

The keyImage field configuration correctly:

  • Uses the "boolean" type for the checkbox UI
  • Provides clear label and description
  • Sets required: false appropriately for an optional feature
  • Omits validation since boolean fields don't need custom validation

131-143: LGTM!

The BooleanField component is correctly implemented as a controlled checkbox with proper type safety. The !!value coercion ensures boolean conversion, and the event handler correctly extracts the checked state.


320-332: LGTM!

The update correctly:

  • Accepts both string and boolean values
  • Only validates string values (since boolean fields don't require validation)
  • Maintains type safety through the conditional check

458-499: LGTM!

The renderField function correctly:

  • Handles the union type string | boolean for field values
  • Uses appropriate type assertions for each field type
  • Branches correctly between template, color, boolean, and text fields
  • Passes properly typed values to each specialized component
apps/obsidian/src/components/canvas/shapes/nodeConstants.ts (2)

1-8: LGTM! Clear documentation and sensible width constraints.

The file header clearly explains the purpose, and the width constants provide reasonable bounds for node rendering.


13-28: LGTM! Font and layout constants are well-defined.

The font sizes correctly match Tailwind classes, the system font stack is appropriate, and the image-related constants provide reasonable constraints. The IMAGE_GAP of 4px keeps the layout compact, which appears intentional.

Copy link
Contributor

@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.

I would suggest not using literal values for the measureNodeText, but other than that, LGTM 👍

@trangdoan982 trangdoan982 merged commit 84813e8 into main Nov 3, 2025
3 checks passed
@github-project-automation github-project-automation bot moved this to Done in General Nov 3, 2025
@trangdoan982 trangdoan982 deleted the eng-869-key-figure branch November 3, 2025 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants