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

Support embedding and isolation of database views within documents and clean up related APIs and context types.

New Features:

  • Allow database blocks to reference multiple database view tabs via view_ids and propagate changes back to block data.
  • Enable creating embedded linked database views and grid/board/calendar databases from the slash menu using new server-side APIs.
  • Render referenced database views distinctly in the outline with dot icons and adjusted child actions.

Bug Fixes:

  • Prevent meta load failures for embedded databases from incorrectly marking database blocks as not found.
  • Fix first websocket reconnect attempt delay and align retry backoff behavior for database loading.
  • Limit AI chat message prefetching to AI chat views only and avoid unnecessary requests.
  • Ensure outline child actions are hidden under database parents and that referenced database views render with correct indentation.

Enhancements:

  • Refine database loading logic to support embedded databases with custom loading strategies and better error handling.
  • Unify database context semantics around databasePageId vs activeViewId and update Yjs selectors and consumers accordingly.
  • Improve HTTP API helpers and page operations to return structured create-page responses and add request logging.
  • Adjust database tab, views, and content components to work with the new database view model and update view ID routing.
  • Use database-aware file URLs by referencing the database page id in file and image components.

Tests:

  • Update HTTP API integration tests and database loading hook tests to reflect new create-page response types and metadata handling.
  • Add Cypress coverage for embedded database view isolation behavior.

Chores:

  • Remove deprecated folder-view creation APIs and related context plumbing now superseded by database view creation.

@sourcery-ai
Copy link

sourcery-ai bot commented Nov 30, 2025

Reviewer's Guide

Refactors database view handling to support embedded database blocks with multiple view tabs and updated APIs, while simplifying page and database creation flows and improving error handling and logging.

Sequence diagram for creating an embedded linked database from the slash menu

sequenceDiagram
  actor User
  participant Editor as Editor_Slate
  participant Slash as SlashPanel
  participant Ctx as EditorContext
  participant API as HTTP_API_Backend

  User->>Editor: Type /linked database
  Editor->>Slash: Open SlashPanel

  Slash->>Ctx: loadViews()
  Ctx-->>Slash: views
  Slash->>Slash: filter databaseViews
  Slash->>Slash: build DatabaseOption list

  User->>Slash: Choose database option
  Slash->>Ctx: loadViewMeta(databaseViewId)
  Ctx->>API: GET /page-view/{databaseViewId}/meta
  API-->>Ctx: meta (database_relations)
  Ctx-->>Slash: meta

  Slash->>Slash: find databaseId via database_relations
  Slash->>Ctx: createDatabaseView(documentId, payload)
  Ctx->>API: POST /workspace/{ws}/page-view/{documentId}/database-view
  API-->>Ctx: CreateDatabaseViewResponse(view_id, database_id)
  Ctx-->>Slash: response

  Slash->>Slash: createDatabaseNodeData(parentId=documentId, viewIds=[response.view_id], databaseId=response.database_id)
  Slash->>Editor: turnInto(blockType, DatabaseNodeData)
  Editor-->>User: Embedded database block appears
Loading

Updated class diagram for database block, loading strategies, and tabs

classDiagram
  class DatabaseNodeData {
    <<interface>>
    +string view_id
    +string[] view_ids
    +string parent_id
    +string database_id
  }

  class databaseBlockUtils {
    +string[] getViewIds(DatabaseNodeData data)
    +string getPrimaryViewId(DatabaseNodeData data)
    +bool hasViewIds(DatabaseNodeData data)
    +DatabaseNodeData createDatabaseNodeData(string parentId, string[] viewIds, string databaseId)
    +DatabaseNodeData addViewId(DatabaseNodeData data, string viewId)
    +DatabaseNodeData removeViewId(DatabaseNodeData data, string viewId)
    +DatabaseNodeData parseDatabaseNodeData(string jsonString)
    +string serializeDatabaseNodeData(DatabaseNodeData data)
  }

  class DatabaseBlock {
    +DatabaseNode node
    +render()
    +handleNavigateToRow(string rowId)
    +handleViewIdsChanged(string[] currentViewIds)
  }

  class useDatabaseLoading {
    +bool notFound
    +YDoc doc
    +string selectedViewId
    +string[] visibleViewIds
    +string databaseName
    +onChangeView(string viewId)
    +loadViewMeta(string viewId, function callback) Promise~View|
  }

  class DatabaseLoadingConfig {
    +string viewId
    +string[] allowedViewIds
    +function loadView(string viewId) Promise~YDoc|
    +function loadViewMeta(string viewId, function callback) Promise~View|
  }

  class DatabaseLoadingStrategy {
    <<interface>>
    +bool shouldSetNotFoundOnMetaError()
    +bool shouldSkipMetaLoad(string id)
    +string[] getVisibleViewIds(View meta)
    +string getDatabaseName(View meta)
  }

  class loadingStrategies {
    +DatabaseLoadingStrategy createEmbeddedDatabaseStrategy(DatabaseLoadingConfig config)
    +DatabaseLoadingStrategy createStandaloneDatabaseStrategy(DatabaseLoadingConfig config)
    +DatabaseLoadingStrategy createLoadingStrategy(DatabaseLoadingConfig config)
    +bool isEmbeddedDatabase(string[] allowedViewIds)
  }

  class DatabaseContextState {
    +bool readOnly
    +YDoc databaseDoc
    +string databasePageId
    +string activeViewId
    +map~RowId, YDoc~ rowDocMap
    +bool isDatabaseRowPage
    +string workspaceId
    +function createDatabaseView(string viewId, CreateDatabaseViewPayload payload) Promise~CreateDatabaseViewResponse|
  }

  class Database {
    +YDoc doc
    +string activeViewId
    +string databasePageId
    +string databaseName
    +string[] visibleViewIds
    +onChangeView(string viewId)
    +onOpenRowPage(string rowId)
  }

  class DatabaseViews {
    +string activeViewId
    +string databasePageId
    +string viewName
    +string[] visibleViewIds
    +onChangeView(string viewId)
    +onViewIdsChanged(string[] viewIds)
  }

  class DatabaseTabs {
    +string[] viewIds
    +string databasePageId
    +string selectedViewId
    +setSelectedViewId(string viewId)
    +onViewIdsChanged(string[] viewIds)
  }

  class DatabaseTabItem {
    +string viewId
    +string databasePageId
    +bool deleteDisabled
  }

  class useAddDatabaseView {
    +addDatabaseView(DatabaseViewLayout layout) Promise~string|
  }

  class SlashPanel {
    +string documentId
    +loadDatabasesForPicker()
    +handleOpenLinkedDatabasePicker(ViewLayout layout, string optionKey)
    +handleSelectDatabase(string targetViewId)
  }

  class EditorContextState {
    +function addPage(string parentId, CreatePagePayload payload) Promise~CreatePageResponse|
    +function createDatabaseView(string viewId, CreateDatabaseViewPayload payload) Promise~CreateDatabaseViewResponse|
    +function loadDatabaseRelations() Promise~DatabaseRelations|
  }

  DatabaseNodeData <.. databaseBlockUtils : uses
  DatabaseBlock ..> DatabaseNodeData : embeds
  DatabaseBlock ..> databaseBlockUtils : uses
  DatabaseBlock ..> useDatabaseLoading : uses
  useDatabaseLoading --> DatabaseLoadingConfig : configured_by
  useDatabaseLoading ..> DatabaseLoadingStrategy : uses
  loadingStrategies ..> DatabaseLoadingStrategy : implements
  loadingStrategies ..> DatabaseLoadingConfig : uses

  DatabaseBlock ..> DatabaseContent : renders
  DatabaseContent ..> Database : renders
  Database ..> DatabaseViews : composes
  DatabaseViews ..> DatabaseTabs : composes
  DatabaseTabs ..> DatabaseTabItem : renders

  Database ..> DatabaseContextState : uses
  DatabaseTabs ..> DatabaseContextState : uses
  useAddDatabaseView ..> DatabaseContextState : uses

  SlashPanel ..> EditorContextState : uses
  SlashPanel ..> databaseBlockUtils : createDatabaseNodeData

  EditorContextState ..> CreatePageResponse : returns
  EditorContextState ..> CreateDatabaseViewResponse : returns

  class CreatePageResponse {
    +string view_id
    +string database_id
  }

  class CreateDatabaseViewPayload {
    +string parent_view_id
    +string database_id
    +ViewLayout layout
    +string name
    +bool embedded
  }

  class CreateDatabaseViewResponse {
    +string view_id
    +string database_id
  }

  DatabaseContextState ..> CreateDatabaseViewPayload : uses
  DatabaseContextState ..> CreateDatabaseViewResponse : returns
  useAddDatabaseView ..> CreateDatabaseViewResponse : uses

  DatabaseTabs ..> CreateDatabaseViewPayload : uses
  DatabaseBlock ..> DatabaseContextState : casts_context
Loading

File-Level Changes

Change Details Files
Add embedded database support with multiple view IDs on blocks and strategy-based loading.
  • Introduce DatabaseNodeData.view_ids/database_id and helpers to create/update/read them while keeping backward compatibility with legacy view_id.
  • Update DatabaseBlock and DatabaseContent to derive active/allowed view IDs from block data, propagate them through Database/DatabaseViews/DatabaseTabs, and sync block data when tabs are added or removed.
  • Refactor useDatabaseLoading to accept allowedViewIds, use strategy objects for embedded vs standalone databases, and relax meta-load failures for embedded databases.
src/application/types.ts
src/components/editor/components/blocks/database/utils/databaseBlockUtils.ts
src/components/editor/components/blocks/database/DatabaseBlock.tsx
src/components/editor/components/blocks/database/components/DatabaseContent.tsx
src/components/editor/components/blocks/database/hooks/useDatabaseLoading.ts
src/components/editor/components/blocks/database/hooks/loadingStrategies.ts
Align database view creation and linking APIs with server expectations and embed semantics.
  • Change createDatabaseView and addAppPage HTTP APIs and service layer to return structured responses including database_id and accept parent_view_id/database_id/embedded flags.
  • Refactor slash menu handlers and DatabaseView hooks to create embedded database views under the correct parent document/database and build DatabaseNodeData via helpers.
  • Remove deprecated createFolderView and getViewIdFromDatabaseId usage, instead resolving database relations via loadViewMeta/loadDatabaseRelations when linking views.
src/application/services/js-services/http/http_api.ts
src/application/services/js-services/index.ts
src/application/services/services.type.ts
src/components/app/hooks/usePageOperations.ts
src/components/editor/components/panels/slash-panel/SlashPanel.tsx
src/components/app/view-actions/AddPageActions.tsx
src/components/app/view-actions/NewPage.tsx
src/components/app/components/DatabaseView.tsx
src/application/database-yjs/dispatch.ts
src/components/app/contexts/BusinessInternalContext.ts
src/components/editor/EditorContext.tsx
src/components/editor/components/panels/mention-panel/MentionPanel.tsx
src/application/services/js-services/http/__tests__/page.integration.test.ts
src/application/services/js-services/http/__tests__/view.integration.test.ts
Clarify database context model (database page vs active view) and propagate IDs across database UI and file handling components.
  • Replace ambiguous iidIndex/viewId usage with databasePageId/activeViewId in Database, DatabaseContextState, selectors, and consumers, including row modals and row-page navigation.
  • Update DatabaseViews, DatabaseTabs, DatabaseViewTabs, and DatabaseTabItem to work with databasePageId, fire onViewIdsChanged when tabs are added/removed, and use the correct IDs when disabling deletion or loading meta.
  • Update file media and relation components to use databasePageId when generating file URLs or resolving related rows, ensuring consistency for embedded and standalone databases.
src/application/database-yjs/context.ts
src/application/database-yjs/selector.ts
src/components/database/Database.tsx
src/components/database/DatabaseViews.tsx
src/components/database/components/DatabaseContent.tsx
src/components/database/components/tabs/DatabaseTabs.tsx
src/components/database/components/tabs/DatabaseViewTabs.tsx
src/components/database/components/tabs/DatabaseTabItem.tsx
src/components/database/components/cell/file-media/FileMediaCell.tsx
src/components/database/components/cell/file-media/PreviewImage.tsx
src/components/database/components/cell/file-media/UnPreviewFile.tsx
src/components/database/components/database-row/file-media/FileMediaCell.tsx
src/components/database/components/database-row/file-media/FileMediaItem.tsx
src/components/database/components/cell/relation/RelationItems.tsx
src/components/database/components/conditions/DatabaseActions.tsx
Tighten outline/AI behaviors and networking robustness.
  • Render referenced database views in the outline with a dot icon and adjusted indentation, propagate parent layout so children under databases hide their overflow menu button.
  • Guard AI chat message prefetching so it only runs on AIChat layouts.
  • Adjust websocket reconnect logic to treat the first reconnect attempt as attempt 0 with a randomized 5–10s delay, and slow down database loading retry backoff to larger intervals.
src/components/app/outline/ViewItem.tsx
src/components/app/header/MoreActions.tsx
src/components/ws/useAppflowyWebSocket.ts
src/components/editor/components/blocks/database/hooks/useRetryFunction.ts

Possibly linked issues

  • #[Bug] Grid not showing database on Web: PR rewires database_id, view_ids, and embedded database loading, which likely fixes the web grid undefined 'id' rendering error.

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

@appflowy appflowy changed the title Embed views feat: support notion style embedded database view Nov 30, 2025
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:

  • There are a lot of new console.debug/console.error calls (e.g. in executeAPIRequest, useDatabaseLoading, SlashPanel, database view creation) that will run in production; consider gating noisy logs behind import.meta.env.DEV or a shared logger so normal usage isn’t flooded with debug output.
  • The new database block utilities (parseDatabaseNodeData, serializeDatabaseNodeData) don’t appear to be used in this diff; if they’re not needed yet, either remove them or add their first call sites so the intended usage is clear.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are a lot of new `console.debug`/`console.error` calls (e.g. in `executeAPIRequest`, `useDatabaseLoading`, `SlashPanel`, database view creation) that will run in production; consider gating noisy logs behind `import.meta.env.DEV` or a shared logger so normal usage isn’t flooded with debug output.
- The new database block utilities (`parseDatabaseNodeData`, `serializeDatabaseNodeData`) don’t appear to be used in this diff; if they’re not needed yet, either remove them or add their first call sites so the intended usage is clear.

## Individual Comments

### Comment 1
<location> `src/components/ws/useAppflowyWebSocket.ts:150` </location>
<code_context>
       setReconnectAttempt(attemptNumber);

       // First attempt: random 5-10s delay (thundering herd prevention)
-      if (attemptNumber === 1) {
+      if (attemptNumber === 0) {
         const firstDelay = 5000 + Math.random() * FIRST_ATTEMPT_MAX_DELAY;

</code_context>

<issue_to_address>
**issue (bug_risk):** Changing the first-attempt check from `=== 1` to `=== 0` may skip the intended thundering-herd backoff.

The reconnect handler now treats `attemptNumber === 0` as the first attempt, but many websocket libraries use `1` as the first reconnect attempt. If `attemptNumber` is 1-based, this condition will never run and the first reconnect won’t get the randomized 5–10s delay for herd prevention. Please confirm the library’s attempt numbering and keep this as `=== 1` (or document the 0-based assumption) if it is 1-based.
</issue_to_address>

### Comment 2
<location> `src/components/editor/components/blocks/database/hooks/useDatabaseLoading.ts:93-95` </location>
<code_context>
+      }
+
       if (id === viewId) {
-        const meta = await retryLoadViewMeta(viewId, updateVisibleViewIds);
+        try {
+          const meta = await retryLoadViewMeta(viewId, updateVisibleViewIds);
+
+          if (meta) {
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Meta-loading path invokes `updateVisibleViewIds` twice for the same meta result.

In `loadViewMetaWithCallback`, when `id === viewId` you pass `updateVisibleViewIds` into `retryLoadViewMeta` and also call `await updateVisibleViewIds(meta)` afterward:
```ts
const meta = await retryLoadViewMeta(viewId, updateVisibleViewIds);
...
if (meta) {
  await updateVisibleViewIds(meta);
  setNotFound(false);
  return meta;
}
```
If `retryLoadViewMeta` invokes its callback with the loaded meta, this leads to two calls to `updateVisibleViewIds` for the same result, causing redundant work and extra state updates. Consider relying on just one: either remove the explicit `await updateVisibleViewIds(meta)` or stop passing `updateVisibleViewIds` into `retryLoadViewMeta` and handle it only here.

```suggestion
      if (id === viewId) {
        try {
          const meta = await retryLoadViewMeta(viewId, updateVisibleViewIds);

          if (meta) {
            setNotFound(false);
            return meta;
          }
        } catch (error) {
```
</issue_to_address>

### Comment 3
<location> `src/components/editor/components/blocks/database/hooks/__tests__/useDatabaseLoading.test.ts:115` </location>
<code_context>
         expect(result.current.visibleViewIds).toEqual(['view-1', 'child-1', 'child-2']);
       });

-      expect(result.current.iidName).toBe('View view-1');
+      expect(result.current.databaseName).toBe('View view-1');
     });

</code_context>

<issue_to_address>
**suggestion (testing):** Extend useDatabaseLoading tests to cover the new embedded-database behavior and loading strategy edge cases.

This hook now has significantly more logic (embedded vs standalone behavior, `allowedViewIds`, and different `notFound` semantics), but the test change here only renames the asserted field.

Please add test coverage for:
- Embedded mode (`allowedViewIds`):
  - `visibleViewIds` derived from `allowedViewIds` even when meta load fails.
  - `selectedViewId` updated when `allowedViewIds` changes and drops the current selection.
  - Base-view meta load errors do **not** set `notFound` and the hook still works with the document alone.
- Standalone mode (no `allowedViewIds`):
  - `notFound` set on meta load failure.
  - `visibleViewIds` from `meta.children` + base `view_id`.
- `shouldSkipMetaLoad` behavior for embedded databases (no meta load for non-base views).

These cases will better exercise the new strategy layer and reduce regression risk in the embedded/standalone logic.

Suggested implementation:

```typescript
        expect(result.current.visibleViewIds).toEqual(['view-1', 'child-1', 'child-2']);
      });


      expect(result.current.databaseName).toBe('View view-1');
    });

    it('should derive visibleViewIds from allowedViewIds in embedded mode even when meta load fails', async () => {
      const allowedViewIds = ['view-1', 'child-1', 'child-2'];

      const { result, waitForNextUpdate } = renderHook(() =>
        useDatabaseLoading({
          // base view is still the same
          viewId: 'view-1',
          // embedded mode: restrict database to the given views
          allowedViewIds,
          // simulate that meta loading fails completely
          loadMeta: jest.fn().mockRejectedValue(new Error('meta failed')),
          // document should still be available and used as the data source
          document: {
            id: 'database-1',
            view_id: 'view-1',
            title: 'Embedded DB',
            children: [],
          } as any,
        }),
      );

      await waitForNextUpdate();

      // visibleViewIds is derived purely from allowedViewIds, even though meta failed
      expect(result.current.visibleViewIds).toEqual(allowedViewIds);
      // embedded mode never considers this a "notFound" just because meta failed
      expect(result.current.notFound).toBe(false);
    });

    it('should update selectedViewId when allowedViewIds changes and drops the current selection', async () => {
      const initialAllowedViewIds = ['view-1', 'child-1', 'child-2'];
      const nextAllowedViewIds = ['view-1', 'child-2']; // child-1 removed

      const wrapperProps = {
        viewId: 'child-1',
        allowedViewIds: initialAllowedViewIds,
        document: {
          id: 'database-1',
          view_id: 'view-1',
          title: 'Embedded DB',
          children: [],
        } as any,
      };

      const { result, rerender, waitForNextUpdate } = renderHook(
        (props: typeof wrapperProps) =>
          useDatabaseLoading({
            ...props,
            loadMeta: jest.fn().mockResolvedValue({
              id: 'database-1',
              name: 'Embedded DB',
              children: [
                { id: 'child-1', name: 'Child 1' },
                { id: 'child-2', name: 'Child 2' },
              ],
            }),
          }),
        { initialProps: wrapperProps },
      );

      await waitForNextUpdate();

      // initial selection is the child view
      expect(result.current.selectedViewId).toBe('child-1');

      // now simulate that the embedding host drops child-1 from allowedViewIds
      rerender({
        ...wrapperProps,
        viewId: 'child-1',
        allowedViewIds: nextAllowedViewIds,
      });

      // selectedViewId should be coerced into the first allowed view (base view)
      expect(result.current.selectedViewId).toBe('view-1');
      expect(result.current.visibleViewIds).toEqual(nextAllowedViewIds);
    });

    it('should not mark embedded databases as notFound when base-view meta load errors', async () => {
      const { result, waitForNextUpdate } = renderHook(() =>
        useDatabaseLoading({
          viewId: 'view-1',
          allowedViewIds: ['view-1'],
          // meta load for the base view fails
          loadMeta: jest.fn().mockRejectedValue(new Error('meta failed')),
          // but the document is still present and should be used
          document: {
            id: 'database-1',
            view_id: 'view-1',
            title: 'Embedded DB',
            children: [],
          } as any,
        }),
      );

      await waitForNextUpdate();

      // even though meta failed, embedded databases should still work and not be "notFound"
      expect(result.current.notFound).toBe(false);
      expect(result.current.visibleViewIds).toEqual(['view-1']);
      expect(result.current.databaseName).toBe('Embedded DB');
    });

    it('should mark standalone databases as notFound when meta load fails', async () => {
      const { result, waitForNextUpdate } = renderHook(() =>
        useDatabaseLoading({
          viewId: 'view-1',
          // standalone mode: no allowedViewIds
          allowedViewIds: undefined,
          // standalone relies on meta; if meta fails we surface notFound
          loadMeta: jest.fn().mockRejectedValue(new Error('meta failed')),
          document: null,
        }),
      );

      await waitForNextUpdate();

      expect(result.current.notFound).toBe(true);
      expect(result.current.visibleViewIds).toEqual([]);
    });

    it('should derive visibleViewIds from meta children and base view in standalone mode', async () => {
      const { result, waitForNextUpdate } = renderHook(() =>
        useDatabaseLoading({
          viewId: 'view-1',
          allowedViewIds: undefined,
          document: null,
          loadMeta: jest.fn().mockResolvedValue({
            id: 'database-1',
            name: 'Standalone DB',
            children: [
              { id: 'child-1', name: 'Child 1' },
              { id: 'child-2', name: 'Child 2' },
            ],
          }),
        }),
      );

      await waitForNextUpdate();

      // visibleViewIds include the base view plus meta children
      expect(result.current.visibleViewIds).toEqual(['view-1', 'child-1', 'child-2']);
      expect(result.current.notFound).toBe(false);
      expect(result.current.databaseName).toBe('Standalone DB');
    });

    it('should skip meta load for non-base views in embedded databases', async () => {
      const loadMeta = jest.fn().mockResolvedValue({
        id: 'database-1',
        name: 'Embedded DB',
        children: [
          { id: 'child-1', name: 'Child 1' },
          { id: 'child-2', name: 'Child 2' },
        ],
      });

      const { result, rerender, waitForNextUpdate } = renderHook(
        ({ viewId }: { viewId: string }) =>
          useDatabaseLoading({
            viewId,
            allowedViewIds: ['view-1', 'child-1', 'child-2'],
            // embedded mode should only load meta for the base view
            loadMeta,
            document: {
              id: 'database-1',
              view_id: 'view-1',
              title: 'Embedded DB',
              children: [],
            } as any,
          }),
        { initialProps: { viewId: 'view-1' } },
      );

      // base view should trigger one meta load
      await waitForNextUpdate();
      expect(loadMeta).toHaveBeenCalledTimes(1);
      expect(loadMeta).toHaveBeenCalledWith(expect.objectContaining({ viewId: 'view-1' }));

      // switch to a child view; embedded mode should not re-load meta
      rerender({ viewId: 'child-1' });

      // no extra meta loads when switching away from base view
      expect(loadMeta).toHaveBeenCalledTimes(1);
      expect(result.current.selectedViewId).toBe('child-1');
      expect(result.current.visibleViewIds).toEqual(['view-1', 'child-1', 'child-2']);
    });

    it('should select first child view when viewId not in visible views', async () => {

```

These edits assume the following about the existing test file and hook signature:

1. `renderHook` from `@testing-library/react` and `useDatabaseLoading` are already imported and available.  
2. The hook accepts a single configuration object with keys `{ viewId, allowedViewIds, loadMeta, document }`.  
3. The hook's `result.current` shape exposes `{ visibleViewIds, selectedViewId, databaseName, notFound }`.  
4. `loadMeta` is an injectable async function used by `useDatabaseLoading` to fetch database meta; the tests here inject `jest.fn()` implementations directly.

To fully integrate these tests, you will likely need to:
1. Align the arguments passed to `useDatabaseLoading` with the actual hook signature (rename or add props such as `databaseId`, `client`, or `services` used in the real code).
2. If the existing tests use a custom helper (e.g. `renderUseDatabaseLoading` or a wrapper to provide providers/mocks), update these new tests to use that helper instead of a raw `renderHook` call.
3. Adjust the shape of `document` and the resolved `loadMeta` payloads to match your real meta/document models (e.g. field names like `view_id` vs `viewId`, `children` structure, etc.).
4. Update the expectation on `loadMeta` arguments in the `should skip meta load for non-base views` test if your meta loader is called with different parameters (e.g. `{ databaseId }` instead of `{ viewId }`), or drop that argument assertion if it is too specific.

Once the hook signature and existing helpers are wired up, these tests will exercise:
- Embedded behavior with `allowedViewIds` and meta failures.
- Selection updates when `allowedViewIds` changes.
- `notFound` semantics for embedded vs standalone.
- `visibleViewIds` composition in standalone mode.
- `shouldSkipMetaLoad` behavior for non-base views in embedded databases.
</issue_to_address>

### Comment 4
<location> `src/application/services/js-services/http/__tests__/page.integration.test.ts:62-63` </location>
<code_context>
             const rootViewId = outline[0]?.view_id || testWorkspaceId;

             const pageName = `Test Page ${Date.now()}`;
-            createdPageId = await APIService.addAppPage(testWorkspaceId, rootViewId, {
+            const { view_id } = await APIService.addAppPage(testWorkspaceId, rootViewId, {
                 layout: 0,
                 name: pageName,
</code_context>

<issue_to_address>
**suggestion (testing):** Broaden integration tests to assert the new CreatePageResponse shape, including database-specific fields.

These tests now destructure `{ view_id }` from the new `CreatePageResponse`, but they still only assert that `view_id` is a string. To align with the new response shape, please add coverage that checks:
- The response includes the expected keys (`view_id` and `database_id` for database pages).
- For non-database pages, `database_id` is absent or `undefined`, matching the API contract.

This can be done by adding a test that creates a database page (e.g., Grid) and asserts `database_id` is present, or by extending an existing test to assert the presence/absence and value of `database_id` for document vs database pages.

Suggested implementation:

```typescript
            const pageName = `Test Page ${Date.now()}`;
            const { view_id, database_id } = await APIService.addAppPage(testWorkspaceId, rootViewId, {
                layout: 0,
                name: pageName,
            });
            createdPageId = view_id;

            expect(createdPageId).toBeDefined();
            expect(typeof createdPageId).toBe('string');
            // Non-database (document) pages should not be associated with a database
            expect(database_id).toBeUndefined();

```

```typescript
            const outline = await APIService.getAppOutline(testWorkspaceId);
            const rootViewId = outline[0]?.view_id || testWorkspaceId;
            const { view_id: pageId, database_id: databaseId } = await APIService.addAppPage(testWorkspaceId, rootViewId, {

```

To fully satisfy the review comment, you should also:

1. Identify the test that creates a database page (e.g., Grid or other database-backed layout). In that test:
   - Ensure the `addAppPage` call passes the correct layout/config for a database page.
   - After the `addAppPage` call, add assertions like:
     ```ts
     expect(databaseId).toBeDefined();
     expect(typeof databaseId).toBe('string');
     ```
   - Optionally, assert that `databaseId` matches any follow-up API calls that operate on that database.

2. If the second `addAppPage` call in the snippet above is *not* the database-page case, locate the appropriate database-page creation test elsewhere in `page.integration.test.ts` and:
   - Destructure `{ view_id, database_id }` from its `addAppPage` response.
   - Add the expectations described above for database pages.

3. If there are other tests in this file that directly or indirectly depend on the shape of `CreatePageResponse`, consider updating them to destructure `database_id` as needed and assert either presence (for database pages) or absence (for document pages), to keep coverage consistent.
</issue_to_address>

### Comment 5
<location> `src/components/editor/components/blocks/database/hooks/useDatabaseLoading.ts:15` </location>
<code_context>
 }

-export const useDatabaseLoading = ({ viewId, loadView, loadViewMeta }: UseDatabaseLoadingProps) => {
+export const useDatabaseLoading = ({ viewId, allowedViewIds, loadView, loadViewMeta }: UseDatabaseLoadingProps) => {
   const [notFound, setNotFound] = useState(false);
   const [doc, setDoc] = useState<YDoc | null>(null);
</code_context>

<issue_to_address>
**issue (complexity):** Consider pushing the embedded vs standalone behaviors (error handling and initial visible view IDs) further into the loading strategy so the hook becomes a thinner, more linear orchestrator with fewer special cases.

You can keep all the new behavior but reduce the “cross‑cutting” complexity by pushing more of the embedded/standalone logic into the strategy itself and de‑duplicating some branches.

### 1. Let the strategy own “embedded vs standalone” error shaping

Right now, meta error handling is split across:

- `handleError``strategy.shouldSetNotFoundOnMetaError()`
- `loadViewMetaWithCallback``isEmbedded` special‑cases (including two `if (isEmbedded) return null` paths)
- `useLayoutEffect``if (!isEmbedded) setNotFound(true)`

You can centralize this in the strategy by adding a small error‑shaping helper and using it in both `loadViewMetaWithCallback` and the initial meta `useLayoutEffect`.

**Strategy interface (small extension):**

```ts
// loadingStrategies.ts
export interface DatabaseLoadingStrategy {
  // ...existing methods
  handleMetaError(error: unknown, ctx: { id: string; isInitial: boolean }): {
    shouldSetNotFound: boolean;
    shouldRethrow: boolean;
    metaFallback: View | null | undefined; // e.g. embedded: null, standalone: undefined
  };
}
```

**Use it in the hook:**

```ts
const loadViewMetaWithCallback = useCallback(
  async (id: string, callback?: (meta: View | null) => void) => {
    if (strategy.shouldSkipMetaLoad(id)) {
      console.debug('[useDatabaseLoading] Skipping meta load (strategy)', { id, viewId });
      return null;
    }

    try {
      const meta = await retryLoadViewMeta(id, id === viewId ? updateVisibleViewIds : callback);
      if (meta && id === viewId) {
        await updateVisibleViewIds(meta);
      }
      setNotFound(false);
      return meta;
    } catch (error) {
      const { shouldSetNotFound, shouldRethrow, metaFallback } = strategy.handleMetaError(error, {
        id,
        isInitial: id === viewId,
      });

      if (shouldSetNotFound) setNotFound(true);
      if (shouldRethrow) throw error;

      return metaFallback ?? null;
    }
  },
  [retryLoadViewMeta, strategy, updateVisibleViewIds, viewId]
);
```

```ts
useLayoutEffect(() => {
  void loadViewMetaWithCallback(viewId)
    .then((meta) => {
      if (!viewIdsRef.current.includes(viewId) && viewIdsRef.current.length > 0) {
        setSelectedViewId(viewIdsRef.current[0]);
      } else {
        setSelectedViewId(viewId);
      }

      if (meta) {
        console.debug('[DatabaseBlock] loaded view meta', {
          viewId,
          children: meta.children?.map((c) => c.view_id),
          name: meta.name,
        });
      }

      setNotFound(false);
    })
    .catch((error) => {
      const { shouldSetNotFound } = strategy.handleMetaError(error, {
        id: viewId,
        isInitial: true,
      });
      if (shouldSetNotFound) setNotFound(true);
    });
}, [loadViewMetaWithCallback, viewId, strategy]);
```

This removes all direct `isEmbedded` branches from the hook and makes the “when do we set `notFound` vs. continue?” logic a single, inspectable place in `loadingStrategies`.

### 2. Move “allowedViewIds on mount” behind the strategy

The combination of:

- `isEmbedded && allowedViewIdsRef.current` in `useLayoutEffect`, and
- the separate `useEffect` syncing `allowedViewIds``visibleViewIds`,

makes the lifecycle harder to follow. You can let the strategy express “use block view IDs immediately” instead of hardcoding embedded + `allowedViewIds` checks.

**Extend strategy:**

```ts
export interface DatabaseLoadingStrategy {
  // ...
  getInitialVisibleViewIds(): string[] | null; // null => let meta decide
}
```

**Hook usage (replacing the `isEmbedded && allowedViewIdsRef.current` block + the extra useEffect):**

```ts
useLayoutEffect(() => {
  const initialIds = strategy.getInitialVisibleViewIds();
  if (initialIds && initialIds.length > 0) {
    setVisibleViewIds(initialIds);
    setSelectedViewId((current) =>
      initialIds.includes(viewId) ? viewId : initialIds[0] ?? current
    );
  }

  void loadViewMetaWithCallback(viewId)
    // ... same as above
}, [loadViewMetaWithCallback, viewId, strategy]);
```

Then the existing `allowedViewIds` sync effect can either be removed or simplified to just update the strategy inputs, depending on how dynamic `allowedViewIds` needs to be.

### 3. Restore a local null guard for `updateVisibleViewIds`

`updateVisibleViewIds` now relies entirely on `strategy.getVisibleViewIds(meta)` handling `null`. Re‑adding a lightweight guard in the hook keeps future strategies from accidentally breaking things:

```ts
const updateVisibleViewIds = useCallback(
  async (meta: View | null) => {
    if (!meta) return;

    const viewIds = strategy.getVisibleViewIds(meta);
    const name = strategy.getDatabaseName(meta);

    setDatabaseName(name);
    setVisibleViewIds(viewIds);
  },
  [strategy]
);
```

This maintains all current behavior but localizes one important invariant (no meta → no update) back into the hook.

---

These changes keep your strategy abstraction and all embedded/standalone behaviors, but:

- Remove duplicated `isEmbedded` / `allowedViewIds` branching from the hook,
- Centralize error semantics and `notFound` decisions in the strategy,
- Make the hook a thinner orchestrator with clearer, linear control flow.
</issue_to_address>

### Comment 6
<location> `src/components/editor/components/blocks/database/hooks/loadingStrategies.ts:17` </location>
<code_context>
+ * Strategy interface for loading database views
+ * This allows different loading behaviors for embedded vs standalone databases
+ */
+export interface DatabaseLoadingStrategy {
+  /**
+   * Whether meta loading errors should trigger notFound state
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying the loading strategy API to a minimal behavior-focused factory and keep straightforward meta-derived transformations directly in the hook instead of hiding them behind strategy methods and duplicate helpers.

You can simplify this without losing any of the behavior you’ve added.

Right now the strategy abstraction is doing three things:

- Holding two booleans (`shouldSetNotFoundOnMetaError`, `shouldSkipMetaLoad`)
- Hiding two simple transformations (`getVisibleViewIds`, `getDatabaseName`)
- Duplicating the “embedded vs standalone” concept with both `createLoadingStrategy` and `isEmbeddedDatabase`

You can keep the configuration flexibility while reducing surface area and indirection by:

1. Returning a minimal shape from `createLoadingStrategy`
2. Moving the simple meta transformations back into the hook (or keeping them as plain helpers)
3. Dropping the separate `isEmbeddedDatabase` export

For example, you can reduce the strategy to just the behavioral flags and keep all data shaping close to where it’s used:

```ts
// types.ts (or same file)
export interface DatabaseLoadingBehavior {
  shouldSetNotFoundOnMetaError: boolean;
  shouldSkipMetaLoad: (id: string) => boolean;
}

export interface DatabaseLoadingConfig {
  viewId: string;
  allowedViewIds?: string[];
  loadView?: (viewId: string) => Promise<YDoc | null>;
  loadViewMeta?: (viewId: string, cb?: (meta: View | null) => void) => Promise<View | null>;
}

export const createLoadingBehavior = (
  config: DatabaseLoadingConfig,
): DatabaseLoadingBehavior => {
  const { viewId, allowedViewIds } = config;
  const isEmbedded = !!allowedViewIds?.length;

  if (isEmbedded) {
    return {
      shouldSetNotFoundOnMetaError: false,
      shouldSkipMetaLoad: (id) => id !== viewId,
    };
  }

  return {
    shouldSetNotFoundOnMetaError: true,
    shouldSkipMetaLoad: () => false,
  };
};
```

Then, in `useDatabaseLoading`, you can keep the meta-derived values inline and explicit, instead of hiding them behind `getVisibleViewIds` / `getDatabaseName`:

```ts
const isEmbedded = !!allowedViewIds?.length;

const visibleViewIds = useMemo(() => {
  if (isEmbedded) {
    return allowedViewIds ?? [];
  }
  if (!meta) {
    return [viewId];
  }
  const ids = meta.children.map((v) => v.view_id);
  ids.unshift(meta.view_id);
  return ids;
}, [isEmbedded, allowedViewIds, meta, viewId]);

const databaseName = meta?.name ?? '';
```

This keeps:

- All embedded vs standalone behavior in one place (the hook + a tiny behavior factory)
- No duplication of the “embedded” concept (`isEmbeddedDatabase` becomes unnecessary)
- Simple transformations easy to see and reason about, without extra indirection

The behavior stays identical, but the abstraction layer is thinner and more focused on the parts that are genuinely conditional, not just data shaping.
</issue_to_address>

### Comment 7
<location> `cypress/e2e/embeded/database/embedded-view-isolation.cy.ts:39-58` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 8
<location> `cypress/e2e/embeded/database/embedded-view-isolation.cy.ts:63-70` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 9
<location> `cypress/e2e/embeded/database/embedded-view-isolation.cy.ts:75-81` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 10
<location> `cypress/e2e/embeded/database/embedded-view-isolation.cy.ts:86-93` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 11
<location> `cypress/e2e/embeded/database/embedded-view-isolation.cy.ts:99-106` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 12
<location> `cypress/e2e/embeded/database/embedded-view-isolation.cy.ts:111-129` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 13
<location> `cypress/e2e/embeded/database/embedded-view-isolation.cy.ts:134-141` </location>
<code_context>

</code_context>

<issue_to_address>
**issue (code-quality):** Avoid function declarations, favouring function assignment expressions, inside blocks. ([`avoid-function-declarations-in-blocks`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-function-declarations-in-blocks))

<details><summary>Explanation</summary>Function declarations may be hoisted in Javascript, but the behaviour is inconsistent between browsers.
Hoisting is generally confusing and should be avoided. Rather than using function declarations inside blocks, you
should use function expressions, which create functions in-scope.
</details>
</issue_to_address>

### Comment 14
<location> `src/components/editor/components/blocks/database/utils/databaseBlockUtils.ts:94-96` </location>
<code_context>
    const data = JSON.parse(jsonString) as DatabaseNodeData;

    return data;

</code_context>

<issue_to_address>
**suggestion (code-quality):** Inline variable that is immediately returned ([`inline-immediately-returned-variable`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/inline-immediately-returned-variable))

```suggestion
    return JSON.parse(jsonString) as DatabaseNodeData;

```

<br/><details><summary>Explanation</summary>Something that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.
</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.

setReconnectAttempt(attemptNumber);

// First attempt: random 5-10s delay (thundering herd prevention)
if (attemptNumber === 1) {
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): Changing the first-attempt check from === 1 to === 0 may skip the intended thundering-herd backoff.

The reconnect handler now treats attemptNumber === 0 as the first attempt, but many websocket libraries use 1 as the first reconnect attempt. If attemptNumber is 1-based, this condition will never run and the first reconnect won’t get the randomized 5–10s delay for herd prevention. Please confirm the library’s attempt numbering and keep this as === 1 (or document the 0-based assumption) if it is 1-based.

@appflowy appflowy merged commit 24cd0cb into main Nov 30, 2025
12 checks passed
@appflowy appflowy deleted the embed_views branch November 30, 2025 14:02
josue693 pushed a commit to josue693/AppFlowy-Web that referenced this pull request Dec 21, 2025
* chore: set databbase id

* chore: support display document based embeded database views

* chore: filter views

* chore: create embedded view

* fix: embedded database view

* refactor: rename DatabaseContextState

* chore: do not show extra when parent is database

* chore: linked database base sidebar prefix

* chore: add tests

* chore: remove unused folder-view api endpoint

* chore: fix retry , add tests

* chore: select view after creating

* chore: clippy

* chore: fix test
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