Skip to content

fix: allow creating and deleting pages in Shared with me - #629

Merged
appflowy merged 4 commits into
mainfrom
codex/c12-shared-page-subpage-repro
Oct 14, 2026
Merged

appflowy merged 4 commits into
mainfrom
codex/c12-shared-page-subpage-repro

Conversation

@appflowy

@appflowy appflowy commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Description

A workspace member with Full Access to a document inside someone else's private space could read and edit it, but Web blocked creating and deleting its subpages under Shared with me. The sidebar omitted those actions, and the page-operation hooks rejected shared pages before contacting the server.

Shared documents now expose Add when the server confirms write access. Shared pages expose Delete when the server confirms Full Access. Each action targets the clicked page, including nested descendants, and uses the existing creation or Move to Trash flow. Guest restrictions and server authorization remain enforced.

  • Resolve permissions only for hovered/open rows, using canonical permissions rather than sidebar access metadata.
  • Keep original page IDs and parents: owners see the subtree inside their private space; members see the same subtree under Shared with me. The private space and unrelated pages remain inaccessible.
  • Refresh shared navigation after rename and deletion. Background refresh failures do not report a completed mutation as failed.
  • Keep the existing import dialog and published-page deletion confirmation behavior.

Reproduction and history

Create an owner-only private space, invite a regular workspace member, and share only one document with Full Access. Before this fix, the member's shared sidebar has no Add/Delete actions, and the hooks reject shared targets even when the server permits the operation.

The shared-page rejection originated in 3e9655502ca085d7b10a4db1aa8170ed734bbe48 (chore: support request access (#84)), authored by Kilu.He on 2025-10-13. This identifies the blocked path, not when C12 first observed it.

Validation

  • 66 unit tests passed across six page-operation, shared-sidebar, and loaded-child suites. Additional permission/navigation suites passed.
  • 3 live Chromium scenarios passed against local Cloud/GoTrue/PostgreSQL. Full Access creates, renames and reloads child/grandchild pages, then trashes the child subtree and verifies owner Trash, canonical hierarchy and removal after reload. Can edit can create but cannot delete; Read Only cannot create or delete. Direct denied requests return 1012 and preserve the pages.
  • Browser checks verify that deleting a child preserves the shared parent and other children, without exposing the private space or unrelated sibling.
  • New deletion regressions failed before the fix at the shared-page hook rejection and missing Delete action. The original creation reproduction failed at the missing Add action.
  • pnpm run lint passed, including error-catalog validation, Web TypeScript and ESLint. git diff --check passed.
pnpm exec jest src/components/app/hooks/__tests__/usePageOperations src/components/app/share-with-me/__tests__/ShareViewItem.test.tsx src/components/app/hooks/__tests__/useWorkspaceData.preserveLoadedChildren.test.ts --runInBand --no-coverage
pnpm exec playwright test playwright/e2e/page/shared-private-page-subpage.spec.ts --workers=1 --reporter=list
pnpm run lint

The browser test uses the existing grantTestWorkspaceProSubscription fixture and CI's APPFLOWY_TEST_POSTGRES_CONTAINER; optional APPFLOWY_TEST_POSTGRES_DATABASE selects the disposable local database. It creates fresh test accounts/workspaces and is not a production diagnostic. Live verification used Cloud source 5c2e155e298e38d3686fe0080d212f9bc2e61185.

Checklist

  • Added relevant comments and unit/browser regression coverage.
  • Verified nested actions, permission denial, rename, deletion and reload.
  • Tested multiple browsers/operating systems — local verification used Chromium on macOS.

Feature Preview

Playwright captures shared-menu, descendant-creation and post-deletion screenshots as test artifacts. This PR changes Web only.

Summary by Sourcery

Enable permission-aware creation, editing, and deletion of shared-page descendants under Shared with me while preserving access isolation and existing server authorization.

New Features:

  • Expose Add and Delete actions for shared pages when canonical server permissions allow the operation.
  • Support creating nested subpages and moving shared descendants to Trash from Shared with me.

Bug Fixes:

  • Allow authorized shared-page mutations while preserving guest restrictions and server-side permission enforcement.
  • Refresh Shared with me navigation after shared-page metadata edits and deletion without turning background refresh failures into mutation failures.

Enhancements:

  • Resolve action permissions per hovered or opened row and preserve canonical page identities and hierarchy across private and shared projections.
  • Reuse existing page creation, import, rename, icon, and deletion flows for shared-page actions.

Tests:

  • Add unit coverage for shared-page creation, metadata refresh, deletion, permission gating, nested targeting, and isolation.
  • Add Chromium end-to-end coverage for Full Access, Can edit, and Read Only shared-page scenarios, including reloads and Trash verification.

@sourcery-ai

sourcery-ai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR enables writable shared documents and their descendants to expose correctly authorized subpage actions, removes the client-side shared-parent block while preserving server and guest restrictions, refreshes shared navigation after renames, and adds unit plus live browser coverage for nested creation and privacy isolation.

Sequence diagram for creating a subpage under a writable shared document

sequenceDiagram
    actor Member
    participant SharedRow as SharedViewItem
    participant Permissions as useViewActionPermissions
    participant Actions as ViewActionsPopover
    participant PageOps as usePageOperations
    participant Server as Page API

    Member->>SharedRow: hover shared document or descendant
    SharedRow->>Permissions: resolve permissions for row view
    Permissions-->>SharedRow: canCreateViewActions
    SharedRow-->>Member: show inline-add-page
    Member->>Actions: click Add
    Actions->>PageOps: createPage(parentViewId)
    PageOps->>Server: POST /v2/page-view/document/default
    Server-->>PageOps: created view receipt
    PageOps-->>Member: open created subpage
Loading

Sequence diagram for renaming a shared descendant

sequenceDiagram
    actor Member
    participant PageOps as usePageOperations
    participant Server as Page API
    participant Cache as ViewService
    participant Outline as Shared navigation

    Member->>PageOps: updatePageName(viewId, name)
    PageOps->>Server: update-name
    Server-->>PageOps: rename succeeded
    PageOps->>Cache: invalidateCache(workspaceId, viewId)
    PageOps->>Outline: loadOutline(workspaceId, false)
    Outline-->>PageOps: refresh result
    PageOps-->>Member: saved rename remains successful
Loading

File-Level Changes

Change Details Files
Expose and authorize subpage actions per shared-sidebar row without eagerly loading permissions for the entire shared tree.
  • Added hover/open-triggered canonical permission checks for document rows.
  • Added Add actions for writable shared documents while retaining guest, read-only, and server-side denial behavior.
  • Passed the existing import handler through Outline and preserved row-specific targets for nested actions.
  • Prevented duplicate expansion state entries.
src/components/app/share-with-me/ShareViewItem.tsx
src/components/app/share-with-me/ShareWithMe.tsx
src/components/app/outline/Outline.tsx
Allow page creation under shared parents and keep shared navigation synchronized after successful mutations.
  • Removed the blanket shared-page creation rejection while retaining the guest restriction and server authorization.
  • Uses the clicked page or descendant as the creation parent.
  • Refreshes Shared with me after successful renames and logs refresh failures without failing the committed rename.
src/components/app/hooks/usePageOperations.ts
Add focused regression coverage for shared-row permissions, nested actions, creation, and rename behavior.
  • Covers writable, read-only, guest, unresolved, loading, revoked, and service-failure cases.
  • Verifies descendant-specific creation, rename, import routing, and active-menu behavior.
  • Verifies shared-outline refresh semantics and preservation of normal outline updates.
src/components/app/hooks/__tests__/usePageOperations.sharedSubpageCreation.test.tsx
src/components/app/share-with-me/__tests__/ShareViewItem.test.tsx
Add live Chromium coverage for page-only sharing across creation, nesting, rename, reload, and isolation boundaries.
  • Creates owner and member fixtures against local Cloud/GoTrue/PostgreSQL services.
  • Validates Full Access creation of child and grandchild and Read Only denial.
  • Checks canonical owner hierarchy, filtered Shared with me hierarchy, inaccessible private space and sibling pages, and attached artifacts.
playwright/e2e/page/shared-private-page-subpage.spec.ts

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

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

## Individual Comments

### Comment 1
<location path="src/components/app/share-with-me/ShareViewItem.tsx" line_range="114" />
<code_context>
+  const { canCreateViewActions, hasLoadedViewActionPermissions, isLoadingViewActionPermissions } =
+    useViewActionPermissions(view, canCreateChildren && (visible || addOpen));
+  const showAdd =
+    canCreateChildren && hasLoadedViewActionPermissions && !isLoadingViewActionPermissions && canCreateViewActions;

   const canEdit = view.access_level && view.access_level > AccessLevel.ReadAndComment;
</code_context>
<issue_to_address>
**Permission lookup failure hides Add**

When the permission service or network fails during the lookup while the member is hovering the row, `useViewActionPermissions` catches the failed permission request, records the view as loaded without permission data, and `showAdd` stays false. The member cannot see Add again until they leave and re-enter the row to trigger another lookup.

Retry the permission lookup after failure or expose a user-visible retry action.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

const { canCreateViewActions, hasLoadedViewActionPermissions, isLoadingViewActionPermissions } =
useViewActionPermissions(view, canCreateChildren && (visible || addOpen));
const showAdd =
canCreateChildren && hasLoadedViewActionPermissions && !isLoadingViewActionPermissions && canCreateViewActions;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium · Permission lookup failure hides Add

When the permission service or network fails during the lookup while the member is hovering the row, useViewActionPermissions catches the failed permission request, records the view as loaded without permission data, and showAdd stays false. The member cannot see Add again until they leave and re-enter the row to trigger another lookup.

Retry the permission lookup after failure or expose a user-visible retry action.

Prompt for AI agents
In `src/components/app/share-with-me/ShareViewItem.tsx` at line 114:

**Permission lookup failure hides Add**

When the permission service or network fails during the lookup while the member is hovering the row, `useViewActionPermissions` catches the failed permission request, records the view as loaded without permission data, and `showAdd` stays false. The member cannot see Add again until they leave and re-enter the row to trigger another lookup.

Retry the permission lookup after failure or expose a user-visible retry action.

@appflowy appflowy changed the title fix: allow subpages under writable shared documents fix: allow creating and deleting pages in Shared with me Oct 14, 2026
@appflowy
appflowy merged commit 05604b3 into main Oct 14, 2026
22 checks passed
@appflowy
appflowy deleted the codex/c12-shared-page-subpage-repro branch October 14, 2026 23:50
appflowy added a commit that referenced this pull request Oct 15, 2026
…ions-implementation

Brings in main 05604b3 (#618, #619, #620, #623, #626, #628, #629, #630)
on top of the permissions branch 23e604e.

Conflicted files and how each was resolved:
- src/@types/translations/en.json: key union. The branch's spaceCreation,
  workspacePermissions, workspaceCreation, groupSpaceAccess and changeFailure
  objects plus main's atomicCreation, userError and userErrorDetail objects.
  No key was changed on both sides; none was lost (checked by script).
- src/application/db/index.ts: main's Dexie version 13 (atomic_creation) is
  kept exactly as released; the branch's sync_permission_blocks version and
  pre-scope cache cleanup move to versions 14 and 15 and also declare
  atomic_creation, so databases at main's 13 or the branch's 13/14 upgrade
  without dropping a table. Collab IndexedDB names compose both fences:
  restore generation outside (serverRestore.cacheKey), server/account/
  workspace scope inside; listing strips the generation, then the scope.
  Scoped provider caches stay; the RID prefix carries the generation. Sign-out
  cleanup only lists and removes names of the current restore generation.
  closeCollabDB keeps the branch's form.
- src/application/session/token.ts (+ token-storage.ts): main's login-lifetime
  witness (captureTokenSession, isTokenSessionCurrent, generation bumps on
  unowned storage changes) moves into token-storage.ts, which now owns
  getTokenParsed/removeStoredToken; token.ts re-exports it and keeps
  invalidTokenForSession and saveGoTrueAuth(token, refreshingSession).
- src/application/services/js-services/http/gotrue.ts: branch refresh logic
  (server-keyed dedup, sibling-tab adoption, AuthSessionChangedError,
  definitive-failure test) plus main's witness: refreshed tokens are saved only
  for the captured login, and a refresh token that is no longer the stored one
  is rejected as AuthSessionChangedError (never signs the newer login out).
  Main's error-identity imports are kept.
- src/application/services/js-services/http/core.ts: both imports; refresh on
  401 still keeps the session on transient/session-changed failures (branch)
  and invalidates only the login that sent the request (main).
- src/application/services/js-services/http/page-api.ts: createSpaceWithInitialPage
  takes main's PageCreationActivation; its assertCurrent combines the
  branch's captured API context (server, account, transport) with the
  caller's owner check. The initial page goes through addAppPage(activation),
  so default pages use the atomic endpoints; pending atomic outcomes keep the
  space; the branch's no-compensation rules for ambiguous outcomes stay.
- .../http/__tests__/page-api.test.ts: branch expectations kept; their composed
  flows now send authored page_data (as main did) so they stay on the legacy
  page-view path, and the workspace check is passed as an activation.
- src/components/app/hooks/usePageOperations.ts: guests are refused with the
  branch's coded denial and no toast; Shared with me parents are no longer
  refused locally (#629, the server decides). Add Page uses main's activation;
  Create space wraps main's page activation with the branch's workspace-scope
  check, and only non-atomic spaces reload the outline.
- src/components/app/view-actions/ViewActionsPopover.tsx: main's shared
  viewActionPermissions plus the branch's single permission read, duplicate
  destination check and Manage space authority (never taken from a shared row).
- src/components/app/view-actions/ManageSpace.tsx: branch's named change
  toasts; the last-owner rule is recognized from getErrorDiagnostic (main),
  since getErrorMessage no longer returns server text.
- src/components/app/view-actions/__tests__/ManageSpace.test.tsx: branch
  expectation (named change toast with the client error's own copy).
- src/components/app/view-actions/__tests__/CreateSpaceModal.test.tsx: branch
  test flows with main's user-facing messages (getErrorMessage never shows raw
  text: generic copy, "Error 422: ...", catalog copy for code 1008).
- src/components/app/settings/billing/SettingsPanelShell.tsx: both imports
  (branch scroll fade, main error catalog revision).
- src/components/app/settings/__tests__/Settings.billing.test.tsx: branch
  capability mock plus main's act import.
- src/components/app/workspaces/Workspaces.tsx: both imports (branch workspace
  creation hooks, main's paid-plan upgrade entry).
- src/components/ws/useWorkspaceRealtimeTransport.ts: leader lock and
  broadcast channel use the branch's server/account/workspace key inside the
  restore generation (both fences).

Semantic decisions:
1. #626 atomic creation vs permission gating: page creation uses main's
   atomic endpoints and journal; the branch's destination checks, space page
   policy and capability hints still run before addPage, and guests get the
   coded denial callers turn into plain copy.
2. #618 public messages vs plain-language helpers: getErrorMessage keeps
   main's contract (public server/catalog copy, never raw text). Branch
   helpers keep their own copy for permission, gone and billing cases and
   read diagnostics only to recognize rules.
3. #630 restore vs storage scoping: every IndexedDB name, lock, channel and
   localStorage key applies the restore generation outside the branch's
   scope. A restore switches to fresh storage; the previous generation's
   copies and unsent edits are neither uploaded nor removed by the new one.
4. #629 Shared with me: members may create and delete there (server decides);
   guests stay refused; P applies only to pages added directly to a space.
5. #623 billing: the upgrade entry hides for paid plans inside the branch's
   redesigned workspace menu; settings panels keep the branch layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
appflowy added a commit that referenced this pull request Oct 15, 2026
- ShareViewItem (main): the shared Add menu now renders the branch's
  permission hook and duplicate-destination check, so the suite mocks the
  app event emitter and keeps the duplicate check (a More-menu action that
  is mocked out) out of the permission calls it inspects.
- usePageOperations.publish (main): an authored initial page still uses the
  composed refresh path, but Create space now always passes the branch's
  workspace check as the activation. A new test stops an authored space
  once its workspace is left while the request is pending.
- usePageOperations.addPageRefusal (branch): members creating under Shared
  with me are no longer refused locally (main #629, the server decides);
  guests still get the coded denial without a toast, also there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant