Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Support for PWA "Share To " on Android #1789

Open
gpapp wants to merge 24 commits intodocmost:mainfrom
gpapp:PWA_ShareTo
Open

Support for PWA "Share To " on Android #1789
gpapp wants to merge 24 commits intodocmost:mainfrom
gpapp:PWA_ShareTo

Conversation

@gpapp
Copy link
Contributor

@gpapp gpapp commented Dec 14, 2025

Added option to create a new subpage in a selected workspace when using Share To Docmost on Android.

  • Created to store shared URLs and text from markdownr (content extraction)
  • If the title and the first line of text match, the first line is removed (workaround for a markdownr bug)
  • Images and other media are NOT supported or tested yet

Tested on Android and self-hosted instance, but uses standard PWA capabilities so, should work for other setups.

Summary by CodeRabbit

  • New Features

    • Web Share Target support to receive shared content via POST.
    • New Share Target page to review shared content, choose space/parent, edit title/content, and import.
    • Service worker added to accept shared POSTs and manage updates (auto-reloads on new version).
    • Import preserves selected parent page and accepts optional title when creating imported pages.
  • Bug Fixes

    • Corrected page search limit behavior.
  • Style

    • App icon entry standardized to 192×192.

✏️ Tip: You can customize this high-level summary in your review settings.

@CLAassistant
Copy link

CLAassistant commented Dec 14, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link

coderabbitai bot commented Dec 14, 2025

Walkthrough

Adds Web Share Target: manifest share_target entry, service worker to accept/cached POST shares and redirect, a new ShareTarget React page and route to import cached content, client service worker registration with update handling, and server import changes to accept and persist an optional parentPageId.

Changes

Cohort / File(s) Change Summary
PWA manifest
apps/client/public/manifest.json
Updated icon size entry and added share_target configuration (POST, enctype, params).
Service worker
apps/client/public/sw.js
New service worker (VERSION = 'v4') with install/activate; handles POST /share-target, parses FormData, caches JSON under Cache API 'share-target' key 'shared-content', and responds with 303 redirect to /share-target.
Client routing & SW registration
apps/client/src/App.tsx, apps/client/src/main.tsx
Added /share-target route for ShareTarget; register /sw.js on window load, listen for updatefound and controllerchange to reload when a new worker activates.
ShareTarget UI
apps/client/src/pages/share-target/share-target.tsx
New React page: reads shared content from URL or Cache API, select space and optional parent page (combobox + navigation/search), edit title/body, and submit multipart import (spaceId, title, file, optional parentPageId) to /pages/import.
Import controller plumbing
apps/server/src/integrations/import/import.controller.ts
Extract parentPageId from uploaded file fields and forward it to ImportService.importPage; switched permission check to Create for Page.
Import service DB logic
apps/server/src/integrations/import/services/import.service.ts
importPage signature extended to accept optional parentPageId and title, returns `{ id, slugId }
Search default fix
apps/server/src/core/search/search.service.ts
Fixed limit defaulting from bitwise (`limit

Sequence Diagram

sequenceDiagram
    autonumber
    participant ExtApp as External App
    participant Browser as Browser
    participant SW as Service Worker
    participant Cache as Cache API
    participant SPA as ShareTarget Page
    participant Server as Server

    ExtApp->>Browser: Share (POST FormData: title,text,url)
    Browser->>SW: POST /share-target
    SW->>Cache: put('shared-content', JSON)
    SW-->>Browser: 303 Redirect -> /share-target
    Browser->>SPA: Load /share-target
    SPA->>Cache: get('shared-content')
    SPA->>User: render import UI (space, parent, title, body)
    User->>SPA: submit import (multipart with parentPageId?)
    SPA->>Server: POST /pages/import (spaceId, parentPageId?, file, title?)
    Server->>Server: importPage(..., parentPageId?, title?) — validate parent, compute position, insert page with parentPageId
    Server-->>SPA: respond with created page id/slug
    SPA->>Browser: navigate to new page
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review focus:
    • apps/client/src/pages/share-target/share-target.tsx — Cache API usage, FormData/multipart construction, UI state and navigation flows.
    • apps/client/public/sw.js — robust FormData parsing, error handling, and cache key semantics.
    • apps/server/src/integrations/import/services/import.service.ts — parentPageId validation, DB insertion (parent linkage), and updated return type.
    • Cross-file consistency of field names (title, text, url, parentPageId, file) and permission change in controller.

Possibly related PRs

Poem

🐰 I hopped through headers, parsed a share,
Cached tiny notes with careful care,
I nudged a page into its nest,
Linked parent branches, did my best,
Now shared crumbs bloom — a cozy lair. 🌿📎

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature addition: PWA Share Target support on Android for creating subpages with shared content.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (4)
apps/client/src/main.tsx (1)

49-97: Prevent SW update reload loops + fix suspicious </BrowserRouter > closing tag.

Current logic can trigger multiple reloads (and in worst cases loops) because you reload both on installed and on controllerchange without a guard.

 root.render(
   <BrowserRouter>
@@
-  </BrowserRouter >,
+  </BrowserRouter>,
 );
 
-if ('serviceWorker' in navigator) {
+if ("serviceWorker" in navigator) {
   window.addEventListener('load', () => {
+    let refreshing = false;
     navigator.serviceWorker.register('/sw.js').then(
       (registration) => {
@@
-            newWorker.addEventListener('statechange', () => {
-              if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
+            newWorker.addEventListener('statechange', () => {
+              if (
+                newWorker.state === 'installed' &&
+                navigator.serviceWorker.controller &&
+                !refreshing
+              ) {
                 // New update available and installed
@@
-                window.location.reload();
+                refreshing = true;
+                window.location.reload();
               }
             });
           }
         });
       },
@@
-    navigator.serviceWorker.addEventListener('controllerchange', () => {
-      window.location.reload();
+    navigator.serviceWorker.addEventListener('controllerchange', () => {
+      if (refreshing) return;
+      refreshing = true;
+      window.location.reload();
     });
   });
 }
apps/server/src/integrations/import/import.controller.ts (1)

75-93: Add validation for parentPageId — ensure parent page exists and belongs to the same space.

The importService.importPage() method accepts parentPageId without validating that it exists or belongs to the specified spaceId. This differs from the standard page creation flow (page.service.ts), which checks both conditions and throws a NotFoundException if the parent doesn't match. An attacker could pass a parentPageId from a different space or a non-existent ID.

Additionally, normalize empty strings to undefined:

-    const parentPageId = file.fields?.parentPageId?.value;
+    const rawParentPageId = file.fields?.parentPageId?.value;
+    const parentPageId =
+      typeof rawParentPageId === 'string' && rawParentPageId.trim()
+        ? rawParentPageId.trim()
+        : undefined;

Then in importService.importPage(), add validation before calling getNewPagePosition():

     if (parentPageId) {
+      const parentPage = await this.pageRepo.findById(parentPageId);
+      if (!parentPage || parentPage.spaceId !== spaceId) {
+        throw new NotFoundException('Parent page not found');
+      }
     }
apps/server/src/integrations/import/services/import.service.ts (2)

84-98: Validate parentPageId (existence + same space/workspace + access) before inserting.
As-is, a client can supply an arbitrary parentPageId; if DB constraints don’t fully prevent it, this risks cross-space linkage and/or unauthorized hierarchy creation.

@@
-        const pagePosition = await this.getNewPagePosition(spaceId, parentPageId);
+        if (parentPageId) {
+          const parent = await this.db
+            .selectFrom('pages')
+            .select(['id', 'spaceId', 'workspaceId'])
+            .where('id', '=', parentPageId)
+            .executeTakeFirst();
+
+          if (!parent || parent.spaceId !== spaceId || parent.workspaceId !== workspaceId) {
+            throw new BadRequestException('Invalid parentPageId');
+          }
+        }
+
+        const pagePosition = await this.getNewPagePosition(spaceId, parentPageId);
@@
-          parentPageId: parentPageId || null,
+          parentPageId: parentPageId ?? null,
         });

(Also consider an explicit authorization check if “can create under parent page” isn’t implied by workspace membership.)


100-102: Log message should use pageTitle (or fileName) instead of possibly-null title.
Current log prints "null.md" when the document has no extracted H1.

-        this.logger.debug(
-          `Successfully imported "${title}${fileExtension}. ID: ${createdPage.id} - SlugId: ${createdPage.slugId}"`,
-        );
+        this.logger.debug(
+          `Successfully imported "${pageTitle}${fileExtension}". ID: ${createdPage.id} - SlugId: ${createdPage.slugId}`,
+        );
🧹 Nitpick comments (1)
apps/server/src/integrations/import/services/import.service.ts (1)

33-40: Constructor brace spacing is non-idiomatic; consider normalizing.
This is purely style, but ) { } sticks out vs typical Nest/TS formatting.

-  ) { }
+  ) {}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 732951a and 95c3238.

📒 Files selected for processing (8)
  • apps/client/public/manifest.json (1 hunks)
  • apps/client/public/sw.js (1 hunks)
  • apps/client/src/App.tsx (2 hunks)
  • apps/client/src/main.tsx (1 hunks)
  • apps/client/src/pages/share-target/share-target.tsx (1 hunks)
  • apps/server/src/core/search/search.service.ts (2 hunks)
  • apps/server/src/integrations/import/import.controller.ts (3 hunks)
  • apps/server/src/integrations/import/services/import.service.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
apps/client/src/pages/share-target/share-target.tsx (4)
apps/client/src/features/search/types/search.types.ts (1)
  • IPageSearch (6-18)
apps/client/src/features/space/queries/space-query.ts (1)
  • useGetSpacesQuery (34-43)
apps/client/src/features/page/queries/page-query.ts (2)
  • useGetRootSidebarPagesQuery (265-277)
  • useGetSidebarPagesQuery (250-263)
apps/server/src/core/search/search.service.ts (1)
  • searchPage (23-134)
apps/client/public/sw.js (1)
apps/server/src/common/interceptors/http-response.interceptor.ts (1)
  • Response (10-12)
apps/client/src/App.tsx (1)
apps/client/src/pages/share-target/share-target.tsx (1)
  • ShareTarget (33-484)
🔇 Additional comments (4)
apps/server/src/core/search/search.service.ts (1)

65-65: The current code on line 65 correctly uses the logical OR operator (||) to default the limit to 25. This ensures that custom limit values are returned as-is (e.g., limit=10 returns 10), while null or undefined values default to 25. No verification of a prior bitwise OR usage was possible due to unavailable git history, and no other similar bitwise OR misuses were found elsewhere in the codebase.

apps/client/src/App.tsx (1)

41-64: Verify /share-target unauthenticated flow (SW may open it before login).

Since the share-target flow can land users on /share-target without an active session, confirm there’s a clean 401 handling path (e.g., redirect to /login and then back). If you intend it to require the app shell, consider nesting under <Layout /> (or whatever enforces auth) rather than a top-level route.

apps/client/public/manifest.json (1)

20-39: Manifest share_target wiring matches the route/SW design.

The share_target block aligns with /share-target and the SW’s POST handling, and limiting params to title/text/url matches the “no media yet” scope.

apps/server/src/integrations/import/services/import.service.ts (1)

178-203: Query logic is correct; Kysely reorders WHERE clauses properly regardless of builder call order.

Verified: Kysely generates SQL with proper clause ordering (SELECT, WHERE, ORDER BY, LIMIT) regardless of the sequence of .where(), .orderBy(), and .limit() method calls. Multiple .where() calls are combined with AND in a single WHERE clause. The code correctly filters by spaceId first, then conditionally adds the parentPageId filter (or IS NULL), ensuring positioned pages are selected only within the same parent context.

Comment on lines +80 to +98
// Fetch spaces
const { data: spacesData, isLoading: isLoadingSpaces } = useGetSpacesQuery({
limit: 100,
});

// Determine effective nav parent for queries
// If we are searching, we don't use these queries for options (we use search results).

// Query for ROOT pages (used when currentNavParentId is null)
const { data: rootPagesData, isLoading: isLoadingRoot } = useGetRootSidebarPagesQuery({
spaceId: selectedSpace || "",
});

// Query for SUB pages (used when currentNavParentId is set)
const { data: subPagesData, isLoading: isLoadingSub } = useGetSidebarPagesQuery({
spaceId: selectedSpace || "",
pageId: currentNavParentId || undefined,
});

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid firing root-pages query with spaceId: "".

useGetRootSidebarPagesQuery({ spaceId: selectedSpace || "" }) can execute with an empty string before the user chooses a space. If that hook is not enabled-guarded internally, it will call the API with an invalid spaceId.

Practical fixes:

  • Add an enabled: !!spaceId option inside useGetRootSidebarPagesQuery (recommended), or
  • Refactor to reuse useGetSidebarPagesQuery for root listings (since it already supports an enabled guard based on spaceId).
🤖 Prompt for AI Agents
In apps/client/src/pages/share-target/share-target.tsx around lines 80 to 98,
the useGetRootSidebarPagesQuery is being called with spaceId: selectedSpace ||
"" which can fire the query with an empty string before a space is chosen;
change the call to include an enabled guard (e.g. enabled: !!selectedSpace) so
the hook only runs when a valid spaceId exists, or alternatively refactor to
call useGetSidebarPagesQuery for root listings (passing pageId undefined) since
that hook already supports an enabled guard based on spaceId.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/client/src/pages/share-target/share-target.tsx (1)

89-97: Queries still fire with empty or invalid spaceId.

Both useGetRootSidebarPagesQuery and useGetSidebarPagesQuery are called with spaceId: selectedSpace || "", which triggers API calls with an empty string before a space is selected.

🧹 Nitpick comments (2)
apps/server/src/integrations/import/services/import.service.ts (2)

90-118: Parent‑aware positioning and return value wiring look good; consider using pageTitle in log

With the scoping bug above fixed, the rest of importPage looks coherent:

  • getNewPagePosition(spaceId, parentPageId) correctly passes through the parent ID so children get their own position sequence.
  • insertPage now persists parentPageId: parentPageId || null, aligning the DB hierarchy with the new positioning logic.
  • The method returns { id, slugId } | null, matching the earlier type mismatch feedback and current usage of createdPage.

Minor tweak you may want:

  • The success log uses the raw title param ("${title}${fileExtension}..."), which may be empty/undefined when the title is derived from content or from fileName. Logging pageTitle instead would better reflect what the user actually sees.

Example adjustment:

-        this.logger.debug(
-          `Successfully imported "${title}${fileExtension}. ID: ${createdPage.id} - SlugId: ${createdPage.slugId}"`,
-        );
+        this.logger.debug(
+          `Successfully imported "${pageTitle}${fileExtension}". ID: ${createdPage.id} - SlugId: ${createdPage.slugId}`,
+        );

186-205: getNewPagePosition correctly scopes by parent; watch DB indexing

The updated getNewPagePosition:

  • Filters by spaceId plus parentPageId when provided, or parentPageId IS NULL for root pages.
  • Still orders by position (collation 'C') and takes the last row to generate a new jittered key.

This matches the hierarchical behavior you want for nested pages.

Operationally, ensure there’s an index that supports this query shape (e.g., on pages(spaceId, parentPageId, position) or similar) so that per‑parent inserts remain cheap as the table grows.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95c3238 and 3a58100.

📒 Files selected for processing (3)
  • apps/client/public/sw.js (1 hunks)
  • apps/client/src/pages/share-target/share-target.tsx (1 hunks)
  • apps/server/src/integrations/import/services/import.service.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/client/public/sw.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-07T21:44:49.482Z
Learnt from: lukasbach
Repo: docmost/docmost PR: 1457
File: apps/client/src/features/page/tree/components/space-tree.tsx:520-525
Timestamp: 2025-08-07T21:44:49.482Z
Learning: In the Docmost headless tree implementation (apps/client/src/features/page/tree/components/space-tree.tsx), the useEffect in the PageArrow component intentionally omits the dependency array. This allows isFolder() to run on subsequent renders as needed, since the item reference is stable and wouldn't trigger re-runs when required. This pattern is intentional for performance reasons in the tree component.

Applied to files:

  • apps/client/src/pages/share-target/share-target.tsx
🧬 Code graph analysis (1)
apps/client/src/pages/share-target/share-target.tsx (4)
apps/client/src/features/search/types/search.types.ts (1)
  • IPageSearch (6-18)
apps/client/src/features/space/queries/space-query.ts (1)
  • useGetSpacesQuery (34-43)
apps/client/src/features/page/queries/page-query.ts (2)
  • useGetRootSidebarPagesQuery (265-277)
  • useGetSidebarPagesQuery (250-263)
apps/server/src/core/search/search.service.ts (1)
  • searchPage (23-134)
🔇 Additional comments (4)
apps/client/src/pages/share-target/share-target.tsx (3)

1-78: Well-structured component with persistent user preferences.

Good use of useLocalStorage for persisting space and parent page selections across sessions. The combobox configuration and state management are properly organized.


123-210: Shared content loading logic handles edge cases well.

The URL-only fallback (lines 186-193), title deduplication from content (lines 153-158), and cache cleanup are all properly implemented. Error notification is appropriately shown to users.


334-492: Clean UI with proper accessibility patterns.

The use of onMouseDown (line 441) for folder navigation correctly prevents the combobox blur event from interfering. The sticky "go up" option and scroll area provide good UX for deep hierarchies.

apps/server/src/integrations/import/services/import.service.ts (1)

33-48: Constructor DI and updated importPage signature look consistent

Constructor DI remains unchanged logically, and the updated importPage signature (adding parentPageId?, title? and returning { id, slugId } | null) is consistent with the later return and with the new parent‑page support.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/server/src/integrations/import/services/import.service.ts (1)

78-86: Critical bug: Variable shadowing prevents page creation when title is empty.

The const destructuring on lines 81-82 creates block-scoped variables that shadow the outer let declarations. When title is null/empty, the outer contentTitle and prosemirrorJson remain undefined, causing if (prosemirrorJson) on line 90 to be false and silently skipping page creation.

Apply this diff to fix the shadowing:

  let contentTitle;
  let prosemirrorJson;
  if (title == null || title === "") {
-   const { title: contentTitle, prosemirrorJson } =
+   const extracted =
      this.extractTitleAndRemoveHeading(prosemirrorState);
+   contentTitle = extracted.title;
+   prosemirrorJson = extracted.prosemirrorJson;
  } else {
    contentTitle = title;
    prosemirrorJson = prosemirrorState;
  }
🧹 Nitpick comments (1)
apps/server/src/integrations/import/services/import.service.ts (1)

114-116: Consider using pageTitle in debug log.

The log uses title (the optional parameter) which may be undefined. Using pageTitle (line 88) would provide more meaningful output.

       this.logger.debug(
-        `Successfully imported "${title}${fileExtension}. ID: ${createdPage.id} - SlugId: ${createdPage.slugId}"`,
+        `Successfully imported "${pageTitle}${fileExtension}". ID: ${createdPage.id} - SlugId: ${createdPage.slugId}`,
       );

Also note there's a missing closing quote before the period.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a58100 and ad893bc.

📒 Files selected for processing (2)
  • apps/server/src/integrations/import/import.controller.ts (3 hunks)
  • apps/server/src/integrations/import/services/import.service.ts (6 hunks)
🔇 Additional comments (3)
apps/server/src/integrations/import/import.controller.ts (1)

77-98: LGTM!

The parentPageId extraction is well-implemented with proper type checking, trimming, and fallback to undefined. Passing it to the service aligns correctly with the updated importPage signature.

apps/server/src/integrations/import/services/import.service.ts (2)

92-97: LGTM!

Good validation ensuring the parent page exists and belongs to the same space before creating a child page. This prevents orphaned pages and cross-space hierarchy issues.


192-209: LGTM!

The getNewPagePosition extension correctly handles both root pages and child pages by conditionally filtering on parentPageId. The query construction with Kysely is sound.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/client/src/pages/share-target/share-target.tsx (1)

88-98: Guard root pages query so it doesn’t fire with an empty spaceId

useGetRootSidebarPagesQuery({ spaceId: selectedSpace || "" }) will run with spaceId: "" before any space is chosen, and the hook itself has no enabled guard. That can trigger an API call with an invalid spaceId.

Consider either:

  • Changing this call to only run when selectedSpace exists (e.g. pass undefined/null and add an enabled option inside useGetRootSidebarPagesQuery), or
  • Reusing useGetSidebarPagesQuery for root listings, which already has an enabled guard on spaceId.
🧹 Nitpick comments (3)
apps/client/src/pages/share-target/share-target.tsx (1)

359-455: Use isSearching for better search UX in the parent page picker

You maintain isSearching state but only show loaders based on isLoadingPages. During a search, the dropdown can briefly show “No pages found” while results are loading.

You can improve this by:

  • Showing a loader when isSearching is true.
  • Suppressing the “No pages found” state while a search is in progress.

For example:

-                                    {isLoadingPages && (
+                                    {(isLoadingPages || isSearching) && (
@@
-                                    {!isLoadingPages && pageItems.length === 0 && (
+                                    {!isLoadingPages && !isSearching && pageItems.length === 0 && (
                                         <Combobox.Empty>{t("No pages found")}</Combobox.Empty>
                                     )}
apps/server/src/integrations/import/services/import.service.ts (2)

92-101: Parent page validation + position lookup are sound; consider also validating workspace

Validating parentPageId via pageRepo.findById and checking parentPage.spaceId === spaceId prevents linking to a parent in another space, and passing parentPageId into getNewPagePosition ensures children are positioned within their parent’s sequence.

Optionally, you might also assert that the parent’s workspaceId matches the provided workspaceId (if not already guaranteed by data model) to harden cross-workspace isolation further.


102-114: Parent-child linkage in insertPage and logging look good, with a tiny log string nit

Setting parentPageId: parentPageId ?? null correctly normalizes the root vs child cases, and the rest of the payload (slugId, title, ydoc, textContent, etc.) is consistent with existing behavior.

Minor nit: the debug string has an extra quote after the filename:

`Successfully imported "${file.filename}. ID: ${createdPage.id} - SlugId: ${createdPage.slugId}"`
                    ^ extra quote before the dot

You probably meant:

`Successfully imported "${file.filename}". ID: ${createdPage.id} - SlugId: ${createdPage.slugId}`
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad893bc and c0152c6.

📒 Files selected for processing (2)
  • apps/client/src/pages/share-target/share-target.tsx (1 hunks)
  • apps/server/src/integrations/import/services/import.service.ts (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-07T21:44:49.482Z
Learnt from: lukasbach
Repo: docmost/docmost PR: 1457
File: apps/client/src/features/page/tree/components/space-tree.tsx:520-525
Timestamp: 2025-08-07T21:44:49.482Z
Learning: In the Docmost headless tree implementation (apps/client/src/features/page/tree/components/space-tree.tsx), the useEffect in the PageArrow component intentionally omits the dependency array. This allows isFolder() to run on subsequent renders as needed, since the item reference is stable and wouldn't trigger re-runs when required. This pattern is intentional for performance reasons in the tree component.

Applied to files:

  • apps/client/src/pages/share-target/share-target.tsx
🧬 Code graph analysis (1)
apps/client/src/pages/share-target/share-target.tsx (4)
apps/client/src/features/search/types/search.types.ts (1)
  • IPageSearch (6-18)
apps/client/src/features/space/queries/space-query.ts (1)
  • useGetSpacesQuery (34-43)
apps/client/src/features/page/queries/page-query.ts (2)
  • useGetRootSidebarPagesQuery (265-277)
  • useGetSidebarPagesQuery (250-263)
apps/server/src/core/search/search.service.ts (1)
  • searchPage (23-134)
🔇 Additional comments (5)
apps/client/src/pages/share-target/share-target.tsx (2)

123-210: Content extraction and URL-only handling look solid

The loadSharedContent flow correctly prefers cached share payloads, falls back to URL params, normalizes the title vs first-line duplication, and now derives a sensible title from url when no title/text are present. The early aborts on fully empty input and the final finally block updating isProcessing all look correct.


267-289: Good handling of empty/failed share cases

The early-return branches for (isProcessing || isLoadingSpaces) and for !sharedData && !isProcessing give clear feedback (“Processing…” vs “No content to share found”) and avoid rendering the full form without data. That guards against weird partial states when cache/URL parsing fails.

apps/server/src/integrations/import/services/import.service.ts (3)

78-90: Title extraction vs provided title is now correct and avoids shadowing

The contentTitle / prosemirrorJson handling now cleanly distinguishes between:

  • No/empty title → use extractTitleAndRemoveHeading result.
  • Provided title → keep full ProseMirror state.

This fixes the previous shadowing issue where outer variables stayed undefined. pageTitle falling back to fileName is also a sensible default.


126-127: Return type now matches actual behavior of importPage

Returning { id, slugId } | null aligns the signature with what callers actually use and avoids the previous Promise<void> vs return-value mismatch.


194-218: Positioning logic for root vs child pages is correct

Updating getNewPagePosition to:

  • Always filter by spaceId, and
  • Add parentPageId filter when provided, or IS NULL for root pages,

ensures new pages are appended at the end of the appropriate sibling list. Using generateJitteredKeyBetween(lastPage.position, null) is consistent with the existing fractional indexing approach.

Comment on lines +219 to +265
// Import Logic
const handleImport = async () => {
if (!selectedSpace || !sharedData) return;

setIsImporting(true);
try {
const { title, text } = sharedData;

const safeTitle = (title || "Shared page")
.replace(/[\\\/:*?"<>|]+/g, "-")
.trim()
.slice(0, 120);
const blob = new Blob([text], { type: "text/markdown" });
const formData = new FormData();
// Page import uses the filename as the title
formData.append("spaceId", selectedSpace);
formData.append("title", title);
formData.append("file", blob, `${safeTitle}.md`);

if (selectedParentPage) {
formData.append("parentPageId", selectedParentPage);
}

const response = await api.post("/pages/import", formData);

const newPage = response.data;

notifications.show({
message: t("Page created successfully"),
});

if (newPage?.slugId) {
navigate(`/s/${selectedSpace}/p/${newPage.slugId}`, { replace: true });
} else {
navigate("/home", { replace: true });
}

} catch (error: any) {
console.error("Import failed:", error);
notifications.show({
message: error?.response?.data?.message || t("Failed to create page"),
color: "red",
});
} finally {
setIsImporting(false);
}
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid double-submits and keep navigation space consistent in handleImport

Two small robustness tweaks here:

  1. The “Save Page” button is still clickable while isImporting is true, so fast taps can create duplicate pages.
  2. navigate uses the current selectedSpace, which might change while the import request is in flight, potentially building a URL with a different space than the one used to create the page.

Suggested adjustments:

-    const handleImport = async () => {
-        if (!selectedSpace || !sharedData) return;
+    const handleImport = async () => {
+        if (!selectedSpace || !sharedData || isImporting) return;
+        const spaceId = selectedSpace;
@@
-            formData.append("spaceId", selectedSpace);
+            formData.append("spaceId", spaceId);
@@
-            if (newPage?.slugId) {
-                navigate(`/s/${selectedSpace}/p/${newPage.slugId}`, { replace: true });
+            if (newPage?.slugId) {
+                navigate(`/s/${spaceId}/p/${newPage.slugId}`, { replace: true });
             } else {
                 navigate("/home", { replace: true });
             }

And optionally also disable the button while importing:

-                        <Button
-                            onClick={handleImport}
-                            loading={isImporting}
-                            disabled={!selectedSpace}
-                        >
+                        <Button
+                            onClick={handleImport}
+                            loading={isImporting}
+                            disabled={!selectedSpace || isImporting}
+                        >

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/client/src/pages/share-target/share-target.tsx (1)

89-91: Still firing root-pages query with empty spaceId.

This issue was flagged in a previous review but remains unresolved. useGetRootSidebarPagesQuery({ spaceId: selectedSpace || "" }) will execute with spaceId: "" when no space is selected, potentially causing an invalid API call.

The recommended fix is to add an enabled guard:

-    const { data: rootPagesData, isLoading: isLoadingRoot } = useGetRootSidebarPagesQuery({
-        spaceId: selectedSpace || "",
-    });
+    const { data: rootPagesData, isLoading: isLoadingRoot } = useGetRootSidebarPagesQuery(
+        { spaceId: selectedSpace || "" },
+        { enabled: !!selectedSpace }
+    );

Alternatively, modify the useGetRootSidebarPagesQuery hook definition to include an internal enabled: !!data.spaceId guard.

🧹 Nitpick comments (3)
apps/client/src/pages/share-target/share-target.tsx (3)

100-124: Minor: search results not cleared when search becomes empty.

When debouncedSearchValue becomes empty or falsy, the effect doesn't execute, leaving old searchResults in state. While line 305 prevents these stale results from being displayed, clearing them would improve state hygiene.

Optional refactor:

     useEffect(() => {
+        if (!debouncedSearchValue || !selectedSpace) {
+            setSearchResults([]);
+            return;
+        }
+
         const fetchSearchResults = async () => {
-            if (debouncedSearchValue && selectedSpace) {
                 const trimmed = debouncedSearchValue.trim();
                 if (trimmed && selectedSpace) {
                     setIsSearching(true);
                     try {
                         const results = await searchPage({
                             query: trimmed,
                             spaceId: selectedSpace,
                         });
                         setSearchResults(results);
                     } catch (error) {
                         console.error("Search failed", error);
                     } finally {
                         setIsSearching(false);
                     }
                 } else {
                     setSearchResults([]);
                 }
-            }
         };
 
         fetchSearchResults();
     }, [debouncedSearchValue, selectedSpace]);

Also note: the selectedSpace check on line 104 is redundant (already checked on line 102).


174-178: Use strict equality operators.

Lines 174 and 177 use loose equality (==) instead of strict equality (===). While functionally equivalent here, strict equality is a best practice in JavaScript/TypeScript.

-                    } else if (url == null || url.trim() == '') {
+                    } else if (url == null || url.trim() === '') {
                         return; // Abort
                     }
-                } else if (url == null || url.trim() == '') {
+                } else if (url == null || url.trim() === '') {
                     return; // Abort
                 }

223-268: Optional: capture selectedSpace before async operations for consistency.

While double-submit prevention is correctly implemented, capturing selectedSpace in a local variable at the start of handleImport would guard against the theoretical race where the user changes the selected space while the import request is in flight.

     const handleImport = async () => {
         if (!selectedSpace || !sharedData || isImporting) return;
+        const spaceId = selectedSpace;
 
         setIsImporting(true);
         try {
             const { title, text } = sharedData;
 
             const safeTitle = (title || "Shared page")
                 .replace(/[\\\/:*?"<>|]+/g, "-")
                 .trim()
                 .slice(0, 120);
             const blob = new Blob([text], { type: "text/markdown" });
             const formData = new FormData();
             // Page import uses the filename as the title
-            formData.append("spaceId", selectedSpace);
+            formData.append("spaceId", spaceId);
             formData.append("title", title);
             formData.append("file", blob, `${safeTitle}.md`);
 
             if (selectedParentPage) {
                 formData.append("parentPageId", selectedParentPage);
             }
 
             const response = await api.post("/pages/import", formData);
 
             const newPage = response.data;
 
             notifications.show({
                 message: t("Page created successfully"),
             });
 
             if (newPage?.slugId) {
-                navigate(`/s/${selectedSpace}/p/${newPage.slugId}`, { replace: true });
+                navigate(`/s/${spaceId}/p/${newPage.slugId}`, { replace: true });
             } else {
                 navigate("/home", { replace: true });
             }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0152c6 and f162687.

📒 Files selected for processing (3)
  • apps/client/src/pages/share-target/share-target.tsx (1 hunks)
  • apps/server/src/integrations/import/import.controller.ts (3 hunks)
  • apps/server/src/integrations/import/services/import.service.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/server/src/integrations/import/import.controller.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-07T21:44:49.482Z
Learnt from: lukasbach
Repo: docmost/docmost PR: 1457
File: apps/client/src/features/page/tree/components/space-tree.tsx:520-525
Timestamp: 2025-08-07T21:44:49.482Z
Learning: In the Docmost headless tree implementation (apps/client/src/features/page/tree/components/space-tree.tsx), the useEffect in the PageArrow component intentionally omits the dependency array. This allows isFolder() to run on subsequent renders as needed, since the item reference is stable and wouldn't trigger re-runs when required. This pattern is intentional for performance reasons in the tree component.

Applied to files:

  • apps/client/src/pages/share-target/share-target.tsx
🧬 Code graph analysis (1)
apps/client/src/pages/share-target/share-target.tsx (4)
apps/client/src/features/search/types/search.types.ts (1)
  • IPageSearch (6-18)
apps/client/src/features/space/queries/space-query.ts (1)
  • useGetSpacesQuery (34-43)
apps/client/src/features/page/queries/page-query.ts (2)
  • useGetRootSidebarPagesQuery (265-277)
  • useGetSidebarPagesQuery (250-263)
apps/server/src/core/search/search.service.ts (1)
  • searchPage (23-134)
🔇 Additional comments (8)
apps/server/src/integrations/import/services/import.service.ts (6)

1-1: LGTM! Import added for parent page validation.

The NotFoundException import is properly used in the parent page validation logic below.


41-48: LGTM! Method signature properly updated.

The signature changes correctly support hierarchical page creation and return the created page identifiers. The optional parameters maintain backward compatibility.


78-90: LGTM! Variable shadowing issue resolved.

The title extraction logic now correctly assigns to the outer variables, fixing the shadowing issue flagged in previous reviews. The conditional logic properly handles both explicit title provision and content-derived titles.


194-218: LGTM! Query logic correctly handles hierarchical positioning.

The conditional filtering properly distinguishes between:

  • Subpage creation: finds the last page with the specified parent
  • Top-level page creation: finds the last page where parentPageId is null

The use of IS NULL for null comparison is correct SQL, and the fractional indexing handles both existing and empty cases.


113-126: LGTM! Page creation and return value properly implemented.

The changes correctly:

  • Persist the parent-child relationship using parentPageId ?? null
  • Log the actual filename for traceability
  • Return the created page identifiers, enabling callers to navigate to the newly created page

94-99: The parent page validation logic is correct. PageRepo.findById returns Page | undefined, and the code properly handles both cases by checking for existence (!parentPage) and verifying space membership (parentPage.spaceId !== spaceId). The spaceId property is non-optional in the Pages schema and always present. The validation prevents orphaned pages and cross-space references as intended.

apps/client/src/pages/share-target/share-target.tsx (2)

1-78: LGTM: Clean imports and well-organized state setup.

The component structure is solid. Persistent state via localStorage for space/parent selection is a good UX choice, and the separation of concerns between processing, importing, and search states is clear.


337-495: LGTM: Well-structured UI with good UX patterns.

The hierarchical page selector with breadcrumb navigation, search, and visual indicators (folder/file icons) provides an intuitive experience. State management correctly resets dependent fields when the space changes, and loading/disabled states are properly handled throughout.

Very minor: the inline styles on line 412 for sticky positioning could be extracted to a constant or style object for consistency, but this is a nitpick.

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