Skip to content

Conversation

@appflowy
Copy link
Contributor

@appflowy appflowy commented Nov 30, 2025

Description


Checklist

General

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the changes in multiple environments (e.g., different browsers, operating systems).

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.

Feature-Specific

  • For feature additions, I've added a preview (video, screenshot, or demo) in the "Feature Preview" section.
  • I've verified that this feature integrates seamlessly with existing functionality.

Summary by Sourcery

Handle unknown editor block types with a dedicated unsupported-block UI and improve testability of the collaborative editor.

New Features:

  • Introduce a styled UnSupportedBlock component that surfaces unsupported block types to users and exposes optional debug details in development mode.

Enhancements:

  • Unify handling of unknown block types by reusing the UnSupportedBlock component from BlockNotFound instead of rendering an invisible placeholder.
  • Add Storybook stories for the UnSupportedBlock component to document and showcase its different usage scenarios.
  • Expose the collaborative editor instance and Yjs document on the window object in dev/test environments to support E2E testing and debugging.

Tests:

  • Add an end-to-end test suite for unsupported editor blocks and wire it up using the newly exposed test editor handles.

@sourcery-ai
Copy link

sourcery-ai bot commented Nov 30, 2025

Reviewer's Guide

Implements a reusable UnSupportedBlock UI for unknown editor block types, wires BlockNotFound to use it, exposes editor/Yjs handles for E2E testing, and adds Storybook stories plus an E2E test scaffold for unsupported blocks.

Sequence diagram for CollaborativeEditor test handle exposure

sequenceDiagram
  participant Browser
  participant CollaborativeEditor
  participant YjsEditor as YjsEditor
  participant YDoc as Y_Doc
  participant Win as Window

  Browser->>CollaborativeEditor: mount component
  activate CollaborativeEditor
  CollaborativeEditor->>YjsEditor: editor.connect(doc)
  activate YjsEditor
  YjsEditor-->>CollaborativeEditor: connected
  deactivate YjsEditor

  Note over CollaborativeEditor: useEffect on editor mount
  CollaborativeEditor->>Win: set __TEST_EDITOR__ = editor
  CollaborativeEditor->>Win: set __TEST_DOC__ = doc
  CollaborativeEditor->>Win: set Y = Y_Module

  Browser-->>CollaborativeEditor: unmount component
  CollaborativeEditor->>YjsEditor: editor.disconnect()
  CollaborativeEditor->>Win: delete __TEST_EDITOR__
  CollaborativeEditor->>Win: delete __TEST_DOC__
  deactivate CollaborativeEditor
Loading

Class diagram for updated unsupported block components

classDiagram
  class BlockNotFound {
    +props EditorElementProps
    +ref HTMLDivElement
    +render(node, children, attributes, ref)
  }

  class UnSupportedBlock {
    +props EditorElementProps
    +className string
    +children ReactNode
    +ref HTMLDivElement
    +isDev boolean
    +render(node, children, className, attributes, ref)
  }

  class UnSupportedBlockDev {
    +props EditorElementProps
    +children ReactNode
    +ref HTMLDivElement
    +render(node, children, ref)
  }

  class CollaborativeEditor {
    +editor YjsEditor
    +doc Y_Doc
    +onEditorConnected function
    +useEffect_connectAndExpose()
  }

  class WindowTestHandles {
    <<interface>>
    +__TEST_EDITOR__ YjsEditor
    +__TEST_DOC__ Y_Doc
    +Y Y_Module
  }

  BlockNotFound --> UnSupportedBlock : uses
  UnSupportedBlock ..> WindowTestHandles : referenced via attributes
  CollaborativeEditor ..> WindowTestHandles : exposes test handles
Loading

File-Level Changes

Change Details Files
Refactor BlockNotFound to delegate unknown block rendering to the new UnSupportedBlock while preserving a dev-only error banner for genuinely missing blocks.
  • Extend BlockNotFound props to forward additional DOM attributes to the underlying element/component.
  • Simplify the dev-only special case: when the node type is 'block_not_found' in development, render a styled Alert explaining the missing block ID and possible reasons.
  • Change the default behavior for non-'block_not_found' nodes to always render UnSupportedBlock with the current node and children instead of a zero-height placeholder div.
src/components/editor/components/element/BlockNotFound.tsx
Redesign UnSupportedBlock into a styled, testable component with optional dev-only debugging and introduce a separate dev-focused variant.
  • Import and use a warning SVG icon and the cn utility to build a consistent, theme-aware container style for unsupported blocks.
  • Update UnSupportedBlock to accept className and spread remaining attributes, set a data-testid, enforce contentEditable=false, and layout icon, text, optional dev debug section, and children in a flex row.
  • Gate a collapsible debug
    Details section behind import.meta.env.DEV to show the full node JSON for developers.
  • Add UnSupportedBlockDev as a separate component that preserves the previous Alert-based warning behavior for potential dev-only usage.
src/components/editor/components/element/UnSupportedBlock.tsx
Expose collaborative editor and Yjs doc instances globally in dev/test modes to support E2E tests and clean them up on unmount.
  • On editor connection, if running in development or test mode, assign the current YjsEditor and Y.Doc instances plus the Y module itself to window-scoped TEST_EDITOR, TEST_DOC, and Y properties.
  • On cleanup, delete TEST_EDITOR and TEST_DOC from window in dev/test modes while leaving Y available for other potential editors or tests.
src/components/editor/CollaborativeEditor.tsx
Add Storybook stories to document and visually test UnSupportedBlock under various node configurations.
  • Create a Storybook meta definition for UnSupportedBlock with centered layout, shared container decorator, and a configurable node arg.
  • Define multiple stories (Default, FutureFeature, DeprecatedFeature, ComplexData, MinimalData, LongTypeName, ThirdPartyIntegration) to exercise a range of node.type values and data payloads.
src/components/editor/components/element/UnSupportedBlock.stories.tsx
Introduce an E2E test spec scaffold for unsupported blocks in the editor.
  • Add a new Cypress spec file placeholder for editor unsupported block behavior tests (implementation not shown in the diff).
cypress/e2e/editor/blocks/unsupported_block.cy.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The BlockNotFound component now always renders UnSupportedBlock in production instead of a zero-height placeholder; double-check that this UX change is intentional and won’t visually regress existing documents with unknown block types.
  • UnSupportedBlockDev appears to be unused after the refactor; consider removing it or wiring it up somewhere to avoid dead code.
  • In UnSupportedBlock, the hardcoded data-testid="unsupported-block" will overwrite any data-testid passed via props; if you need both behaviors, consider composing the test id or only applying the default when one isn’t provided.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `BlockNotFound` component now always renders `UnSupportedBlock` in production instead of a zero-height placeholder; double-check that this UX change is intentional and won’t visually regress existing documents with unknown block types.
- `UnSupportedBlockDev` appears to be unused after the refactor; consider removing it or wiring it up somewhere to avoid dead code.
- In `UnSupportedBlock`, the hardcoded `data-testid="unsupported-block"` will overwrite any `data-testid` passed via props; if you need both behaviors, consider composing the test id or only applying the default when one isn’t provided.

## Individual Comments

### Comment 1
<location> `src/components/editor/components/element/UnSupportedBlock.tsx:39` </location>
<code_context>
+  );
+});
+
+export const UnSupportedBlockDev = forwardRef<HTMLDivElement, EditorElementProps>(({ node, children }, ref) => {
   return (
     <div className={'w-full select-none'} ref={ref} contentEditable={false}>
</code_context>

<issue_to_address>
**issue (complexity):** Consider consolidating the dev-only and legacy unsupported block behavior into a single `UnSupportedBlock` component with a layout variant instead of maintaining two separate components.

The extra `UnSupportedBlockDev` component does add avoidable complexity and duplicate responsibility. You can keep all current behavior while simplifying by consolidating into a single component and making the “legacy” layout a variant.

For example, instead of exporting two components:

```ts
export const UnSupportedBlock = forwardRef<HTMLDivElement, EditorElementProps>(
  ({ node, children, className, ...attributes }, ref) => {
    const isDev = import.meta.env.DEV;

    return (
      <div
        ref={ref}
        {...attributes}
        data-testid="unsupported-block"
        className={cn(
          className,
          'my-1 flex w-full select-none items-center gap-2 rounded-lg border border-line-divider bg-fill-list-hover px-3 py-2 text-text-caption'
        )}
        contentEditable={false}
      >
        <WarningIcon className="h-5 w-5 flex-shrink-0 text-function-warning" />
        <span className="text-sm">
          This block type <span className="font-medium text-text-title">&ldquo;{node.type}&rdquo;</span> is not supported yet!
        </span>
        {isDev && (
          <details className="ml-auto text-xs">
            <summary className="cursor-pointer text-text-caption hover:text-text-title">Debug</summary>
            <pre className="mt-2 max-h-40 overflow-auto rounded bg-bg-body p-2">
              <code>{JSON.stringify(node, null, 2)}</code>
            </pre>
          </details>
        )}
        {children}
      </div>
    );
  }
);

export const UnSupportedBlockDev = forwardRef<HTMLDivElement, EditorElementProps>(
  ({ node, children }, ref) => {
    // legacy Alert-based UI
  }
);
```

You can have a single component with a variant flag (or an env-based switch) that preserves the old behavior when needed:

```ts
type UnSupportedBlockProps = EditorElementProps & {
  legacyLayout?: boolean;
};

export const UnSupportedBlock = forwardRef<HTMLDivElement, UnSupportedBlockProps>(
  ({ node, children, className, legacyLayout = false, ...attributes }, ref) => {
    const isDev = import.meta.env.DEV;

    if (legacyLayout) {
      return (
        <div className="w-full select-none" ref={ref} contentEditable={false} {...attributes}>
          <Alert className="h-fit w-full" severity="warning">
            <div className="text-base font-semibold">{`Unsupported Block: ${node.type}`}</div>
            <div className="my-4 whitespace-pre font-medium">
              {`We're sorry for inconvenience \n`}
              Submit an issue on our{' '}
              <a className="text-text-action underline" /* ... */>
                GitHub
              </a>
            </div>
            {children}
          </Alert>
        </div>
      );
    }

    return (
      <div
        ref={ref}
        {...attributes}
        data-testid="unsupported-block"
        className={cn(
          className,
          'my-1 flex w-full select-none items-center gap-2 rounded-lg border border-line-divider bg-fill-list-hover px-3 py-2 text-text-caption'
        )}
        contentEditable={false}
      >
        <WarningIcon className="h-5 w-5 flex-shrink-0 text-function-warning" />
        <span className="text-sm">
          This block type <span className="font-medium text-text-title">&ldquo;{node.type}&rdquo;</span> is not supported yet!
        </span>
        {isDev && (
          <details className="ml-auto text-xs">
            <summary className="cursor-pointer text-text-caption hover:text-text-title">Debug</summary>
            <pre className="mt-2 max-h-40 overflow-auto rounded bg-bg-body p-2">
              <code>{JSON.stringify(node, null, 2)}</code>
            </pre>
          </details>
        )}
        {children}
      </div>
    );
  }
);
```

Actionable steps:

1. Remove `UnSupportedBlockDev` if it’s not used anywhere, **or** move its JSX into a `legacyLayout` (or `variant`) branch on `UnSupportedBlock`.
2. If you want it dev-only, gate `legacyLayout` behind an env flag where it’s consumed (or derive it from `import.meta.env.DEV` inside the component).
3. Keep all dev-specific behavior (`Debug` details + any legacy layout) scoped to this single component so future readers don’t have to reason about two overlapping exports.
</issue_to_address>

### Comment 2
<location> `cypress/e2e/editor/blocks/unsupported_block.cy.ts:80` </location>
<code_context>
        const Y = testWindow.Y;

</code_context>

<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
        const {Y} = testWindow;
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### Comment 3
<location> `cypress/e2e/editor/blocks/unsupported_block.cy.ts:162` </location>
<code_context>
        const Y = testWindow.Y;

</code_context>

<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
        const {Y} = testWindow;
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

### Comment 4
<location> `cypress/e2e/editor/blocks/unsupported_block.cy.ts:235` </location>
<code_context>
        const Y = testWindow.Y;

</code_context>

<issue_to_address>
**suggestion (code-quality):** Prefer object destructuring when accessing and using properties. ([`use-object-destructuring`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/use-object-destructuring))

```suggestion
        const {Y} = testWindow;
```

<br/><details><summary>Explanation</summary>Object destructuring can often remove an unnecessary temporary reference, as well as making your code more succinct.

From the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/#destructuring--object)
</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@appflowy appflowy merged commit 6d040f8 into main Nov 30, 2025
9 of 12 checks passed
@appflowy appflowy deleted the build_unsupport_block branch November 30, 2025 12:28
josue693 pushed a commit to josue693/AppFlowy-Web that referenced this pull request Dec 21, 2025
* chore: build unsupport block

* chore: clippy
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