Skip to content

Fix crash removing link mark when no link is active - #2802

Open
bengotow wants to merge 1 commit into
masterfrom
claude/awesome-ritchie-rj8l4o
Open

Fix crash removing link mark when no link is active#2802
bengotow wants to merge 1 commit into
masterfrom
claude/awesome-ritchie-rj8l4o

Conversation

@bengotow

Copy link
Copy Markdown
Collaborator

Summary

Fixes MAILSPRING-CLIENT-10Error: Mark.fromJS requires a type string, thrown from deep inside Slate/slate-react (151 occurrences, 63 users impacted, ongoing since May 2026).

Root cause

In app/src/components/composer-editor/toolbar-component-factories.tsx, BuildMarkButtonWithValuePicker's onRemove handler looks up the currently active mark and unconditionally passes it to Slate:

onRemove = (e: React.MouseEvent) => {
  e.preventDefault();
  const { value, editor } = this.props;
  const active = safeActiveMarks(value).find((m) => m.type === config.type);
  if (value.selection.isCollapsed) {
    const anchorNode = value.document.getNode(value.selection.anchor.key);
    const expanded = value.selection.moveToRangeOfNode(anchorNode);
    editor.removeMarkAtRange(expanded as any, active); // active can be undefined
  } else {
    editor.removeMark(active); // active can be undefined
  }
};

active is undefined whenever there is no link mark on the current selection. onRemove is called from onConfirm any time the user confirms the link popover (presses Enter, or clicks "Add") with an empty URL field — a common path, since the Link button is usually clicked with a collapsed cursor to insert a brand-new link, and it's easy to press Enter/click Add before typing a URL.

Slate's Mark.create(properties = {}) silently defaults undefined to {}, which passes its "is a plain object" check and reaches Mark.fromJSON({}), where type is undefined, throwing Mark.fromJS() requires a type string. deep inside slate/slate-react, several frames away from the actual app-code bug — which is why the captured stack trace contains only library frames.

The sibling helper removeMarksOfTypeInRange in the same file already guards this exact case (if (active) { editor.removeMark(active); }), but onRemove was missing the same check.

Fix

Add the missing guard: if there's no active mark to remove, focus the editor and return instead of calling into Slate with undefined.

Test plan

  • ./node_modules/.bin/tsc -p app/tsconfig.json --noEmit — passes
  • node_modules/.bin/eslint -c .eslintrc app/src/components/composer-editor/toolbar-component-factories.tsx — passes
  • Manual: open composer, click the Link toolbar button with cursor placed in text (no selection), press Enter/click Add without typing a URL — previously threw, now no-ops as expected

Generated by Claude Code

The link toolbar's onRemove handler called editor.removeMark()/
removeMarkAtRange() with the result of an unguarded Array.find(),
which is undefined whenever the selection has no active link mark.
Slate's Mark.create() silently defaults an undefined argument to {}
and passes it to Mark.fromJSON(), which throws "Mark.fromJS()
requires a type string." This is reached whenever a user opens the
Link popover and confirms an empty URL field without ever having
applied a link (Enter or the Add button triggers onConfirm, which
delegates to onRemove when the field is blank).

Guard onRemove the same way the neighboring removeMarksOfTypeInRange
helper already does: no-op when there's no active mark to remove.
@indent-staging

indent-staging Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Warning

Indent Zero is shutting down on August 7th. Please migrate over to Indent 2.0 to continue getting PR reviews.

PR Summary

Fixes a reproducible crash in the composer's link toolbar: opening the Link popover on a selection with no existing link and confirming an empty URL previously threw Mark.fromJS() requires a 'type' string inside Slate. The onRemove handler now no-ops (after restoring editor focus) when there's no active mark of the configured type, matching the guard pattern already used by removeMarksOfTypeInRange. Existing behavior for clearing an already-applied link is preserved because that path still has a defined active mark.

  • app/src/components/composer-editor/toolbar-component-factories.tsx: added an early-return guard in BuildMarkButtonWithValuePicker.onRemove that calls editor.focus() and returns when safeActiveMarks(value).find(...) yields undefined, preventing editor.removeMark(undefined) / editor.removeMarkAtRange(..., undefined) from being called.

Issues

No issues found.

CI Checks

All CI checks passed for commit 8f64b22.

Custom Rules 3 rules evaluated, 3 passed, 0 failed

Passing This is a longer title to see what happens when they are too long to fit
Passing B
Passing Ben Rule

View all rules

@indent

indent Bot commented Aug 17, 2026

Copy link
Copy Markdown
PR Summary

Fixes the Mark.fromJS requires a type string crash (MAILSPRING-CLIENT-10) that occurred when a user confirmed the link popover with an empty URL field and no active link mark. onRemove previously passed an undefined mark into Slate's removeMark/removeMarkAtRange, which threw deep inside Mark.fromJSON.

  • Added an early-return guard in BuildMarkButtonWithValuePicker.onRemove that focuses the editor and returns when there is no active mark of the configured type, matching the existing guard in the sibling removeMarksOfTypeInRange helper.

Issues

No issues found.

CI Checks

All CI checks passed for 8f64b22.

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