Skip to content

Conversation

@appflowy
Copy link
Contributor

@appflowy appflowy commented Dec 1, 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 database mappings when duplicating published views so template-based duplicates open correctly in the web app.

New Features:

  • Return structured duplicate publish view response including view ID and database-to-view mappings from the backend API and surface it through the client service.

Bug Fixes:

  • Ensure duplicated published views created from templates can resolve their parent database immediately when opened in the app by passing and consuming database mappings.

Enhancements:

  • Persist database-to-view mappings for duplicated templates in URL parameters and localStorage to avoid relying solely on workspace database sync.
  • Update publish duplication flow and service types to work with the richer duplicate publish view response instead of just a bare view ID.

@sourcery-ai
Copy link

sourcery-ai bot commented Dec 1, 2025

Reviewer's Guide

Wire template duplication to return and propagate database/view mappings, pass them via URL/localStorage, and use them to resolve the correct database doc when opening the duplicated view, plus update typings accordingly.

Sequence diagram for template duplication and database mappings propagation

sequenceDiagram
    actor User
    participant DuplicateModal
    participant AFClientService
    participant APIService
    participant Backend
    participant BrowserLocation
    participant useViewOperations
    participant LocalStorage

    User->>DuplicateModal: clickDuplicate()
    DuplicateModal->>AFClientService: duplicatePublishView(workspaceId, spaceViewId, viewId, collabType)
    AFClientService->>APIService: duplicatePublishView(workspaceId, payload)
    APIService->>Backend: POST /api/workspace/{id}/published-duplicate
    Backend-->>APIService: DuplicatePublishViewResponse(view_id, database_mappings)
    APIService-->>AFClientService: DuplicatePublishViewResponse(view_id, database_mappings)
    AFClientService-->>DuplicateModal: {viewId, databaseMappings}

    DuplicateModal->>DuplicateModal: setNewViewId(viewId)
    DuplicateModal->>DuplicateModal: setDatabaseMappings(databaseMappings)

    User->>DuplicateModal: confirmSuccessModalOpenInApp()
    DuplicateModal->>BrowserLocation: open(/app/{workspaceId}/{viewId}?db_mappings=encodedMappings)

    BrowserLocation->>useViewOperations: loadView(viewId)
    useViewOperations->>BrowserLocation: read URLSearchParams(db_mappings)
    alt db_mappings present
        useViewOperations->>useViewOperations: parse db_mappings
        useViewOperations->>LocalStorage: getItem(db_mappings_workspaceId)
        LocalStorage-->>useViewOperations: existingMappings
        useViewOperations->>LocalStorage: setItem(mergedMappings)
        useViewOperations-->>useViewOperations: find databaseId containing viewId
    else db_mappings absent
        useViewOperations->>LocalStorage: getItem(db_mappings_workspaceId)
        LocalStorage-->>useViewOperations: cachedMappings or null
        useViewOperations-->>useViewOperations: try find databaseId containing viewId
        opt not found in mappings
            useViewOperations->>AFClientService: registerWorkspaceDatabaseDocIfNeeded()
            AFClientService-->>useViewOperations: workspaceDatabaseDoc
            useViewOperations-->>useViewOperations: find databaseId via workspace database
        end
    end
    useViewOperations-->>BrowserLocation: resolved databaseId for view
Loading

Updated class diagram for duplicate publish view types and services

classDiagram
    class DuplicatePublishViewPayload {
      +string published_view_id
      +string dest_view_id
      +string published_collab_type
    }

    class HttpDuplicatePublishViewResponse {
      +string view_id
      +Record~string, string[]~ database_mappings
    }

    class TypesDuplicatePublishView {
      +string workspaceId
      +string spaceViewId
      +string viewId
      +string collabType
    }

    class TypesDuplicatePublishViewResponse {
      +string viewId
      +Record~string, string[]~ databaseMappings
    }

    class APIService {
      +duplicatePublishView(workspaceId string, payload DuplicatePublishViewPayload) HttpDuplicatePublishViewResponse
    }

    class AFClientService {
      +duplicatePublishView(params TypesDuplicatePublishView) Promise~TypesDuplicatePublishViewResponse~
    }

    class PublishService {
      <<interface>>
      +duplicatePublishView(params TypesDuplicatePublishView) Promise~TypesDuplicatePublishViewResponse~
    }

    class DuplicateModal {
      -boolean open
      -string viewId
      -string selectedWorkspaceId
      -string selectedSpaceId
      -boolean loading
      -boolean successModalOpen
      -string newViewId
      -Record~string, string[]~ databaseMappings
      +handleDuplicate()
      +handleOpenInApp()
    }

    APIService <.. AFClientService : uses
    AFClientService ..|> PublishService : implements
    AFClientService --> TypesDuplicatePublishViewResponse : returns
    AFClientService --> TypesDuplicatePublishView : takes
    APIService --> HttpDuplicatePublishViewResponse : returns
    APIService --> DuplicatePublishViewPayload : takes
    DuplicateModal --> AFClientService : calls duplicatePublishView
    DuplicateModal --> TypesDuplicatePublishViewResponse : stores
Loading

File-Level Changes

Change Details Files
Use URL/localStorage db_mappings to resolve databaseId for a view before falling back to workspace database sync.
  • Before registering/syncing workspace database docs, attempt to read a db_mappings parameter from window.location.search and parse it as database-to-view IDs mapping.
  • Persist any db_mappings from the URL into localStorage under a workspace-scoped key, merging with existing mappings if present.
  • On failure to parse URL or localStorage JSON, log a warning but continue gracefully.
  • If a mapping contains the requested view ID, immediately return the corresponding databaseId, avoiding the need to wait for sync.
src/components/app/hooks/useViewOperations.ts
Propagate duplicate publish view response (viewId + databaseMappings) through the service layer and use it in the duplicate modal to construct the open-in-app URL with db_mappings.
  • Change HTTP API duplicatePublishView to return a structured DuplicatePublishViewResponse (view_id and database_mappings) instead of just the view_id string.
  • Add a frontend DuplicatePublishViewResponse type and update AFClientService.duplicatePublishView to adapt snake_case API response to camelCase and ensure databaseMappings defaults to an empty object.
  • Update PublishService interface signature so duplicatePublishView returns DuplicatePublishViewResponse.
  • Update DuplicateModal to track both newViewId and databaseMappings from the service response, reset them on error, and when opening the app, append a db_mappings query param (URL-encoded JSON) when mappings exist.
src/application/services/js-services/http/http_api.ts
src/application/services/js-services/index.ts
src/application/types.ts
src/application/services/services.type.ts
src/components/publish/header/duplicate/DuplicateModal.tsx

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 and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/components/app/hooks/useViewOperations.ts:45` </location>
<code_context>
     async (id: string) => {
       if (!currentWorkspaceId) return;

+      // First check URL params for database mappings (passed from template duplication)
+      // This allows immediate lookup without waiting for workspace database sync
+      try {
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting the repeated mapping lookup, URL parsing, and localStorage handling into small helper functions so `getDatabaseId` just orchestrates the steps at a higher level.

You can keep all the new behavior but reduce complexity by extracting the duplicated mapping/lookup logic into small helpers. That will also hide the try/catch noise and side effects from `getDatabaseId`.

### 1. Extract shared “find DB by viewId” logic

Both URL and localStorage blocks do:

- Parse JSON into `Record<string, string[]>`
- Loop `Object.entries` and find matching `viewId`

You can centralize that:

```ts
function findDatabaseIdForView(
  mappings: Record<string, string[]>,
  viewId: string
): string | null {
  for (const [databaseId, viewIds] of Object.entries(mappings)) {
    if (viewIds.includes(viewId)) {
      return databaseId;
    }
  }
  return null;
}
```

### 2. Extract URL + storage handling into small helpers

Move the JSON parsing, storage access, and try/catch into focused helpers. This keeps `getDatabaseId` linear and declarative while preserving all side effects.

```ts
const getDbMappingsStorageKey = (workspaceId: string) =>
  `db_mappings_${workspaceId}`;

function readDbMappingsFromUrl(
  workspaceId: string
): { mappings: Record<string, string[]> | null; source: 'url' | null } {
  try {
    const urlParams = new URLSearchParams(window.location.search);
    const dbMappingsParam = urlParams.get('db_mappings');
    if (!dbMappingsParam) return { mappings: null, source: null };

    const dbMappings: Record<string, string[]> = JSON.parse(
      decodeURIComponent(dbMappingsParam)
    );

    const storageKey = getDbMappingsStorageKey(workspaceId);
    const existingMappings = JSON.parse(localStorage.getItem(storageKey) || '{}');
    const mergedMappings = { ...existingMappings, ...dbMappings };

    localStorage.setItem(storageKey, JSON.stringify(mergedMappings));
    console.debug(
      '[useViewOperations] stored db_mappings to localStorage',
      mergedMappings
    );

    return { mappings: dbMappings, source: 'url' };
  } catch (e) {
    console.warn('[useViewOperations] failed to parse db_mappings from URL', e);
    return { mappings: null, source: null };
  }
}

function readDbMappingsFromStorage(
  workspaceId: string
): Record<string, string[]> | null {
  try {
    const storageKey = getDbMappingsStorageKey(workspaceId);
    const cachedMappings = localStorage.getItem(storageKey);
    if (!cachedMappings) return null;

    return JSON.parse(cachedMappings) as Record<string, string[]>;
  } catch (e) {
    console.warn(
      '[useViewOperations] failed to read db_mappings from localStorage',
      e
    );
    return null;
  }
}
```

### 3. Simplify `getDatabaseId` by composing helpers

The core hook logic then becomes easier to scan and reason about:

```ts
const getDatabaseId = useCallback(
  async (id: string) => {
    if (!currentWorkspaceId) return null;

    // URL mappings (also persists to localStorage)
    const { mappings: urlMappings } = readDbMappingsFromUrl(currentWorkspaceId);
    if (urlMappings) {
      const dbId = findDatabaseIdForView(urlMappings, id);
      if (dbId) {
        console.debug('[useViewOperations] found databaseId from URL params', {
          viewId: id,
          databaseId: dbId,
        });
        return dbId;
      }
    }

    // Cached mappings
    const cachedMappings = readDbMappingsFromStorage(currentWorkspaceId);
    if (cachedMappings) {
      const dbId = findDatabaseIdForView(cachedMappings, id);
      if (dbId) {
        console.debug(
          '[useViewOperations] found databaseId from localStorage',
          { viewId: id, databaseId: dbId }
        );
        return dbId;
      }
    }

    if (databaseStorageId && !workspaceDatabaseDocMapRef.current.has(currentWorkspaceId)) {
      await registerWorkspaceDatabaseDoc(currentWorkspaceId, databaseStorageId);
    }

    // existing Yjs-based resolution unchanged...
    return new Promise<string | null>((resolve) => {
      // ...
    });
  },
  [currentWorkspaceId, databaseStorageId, registerWorkspaceDatabaseDoc]
);
```

This keeps all existing behavior (URL-first lookup, localStorage merge, logging, fallback to workspace doc) while:

- Removing duplicated parsing/iteration code
- Localizing side effects and try/catch blocks in small helpers
- Making `getDatabaseId` responsible primarily for orchestration, not low-level details.
</issue_to_address>

### Comment 2
<location> `cypress/e2e/page/template-duplication.cy.ts:49-63` </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 3
<location> `cypress/e2e/page/template-duplication.cy.ts:68-90` </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 4
<location> `cypress/e2e/page/template-duplication.cy.ts:221` </location>
<code_context>
        const origin = win.location.origin;

</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 {origin} = win.location;
```

<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 6ae43aa into main Dec 1, 2025
12 checks passed
@appflowy appflowy deleted the fix_use_template branch December 1, 2025 06:39
josue693 pushed a commit to josue693/AppFlowy-Web that referenced this pull request Dec 21, 2025
* chore: fix use template

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