Outside the editor the UI did background work on every tree event, socket
reconnect, and navigation. Tree infra (virtualization/memo/O(N) utils) was
already good — the cost was in the subscriptions and duplicates around it.
Client-only; behavior 1:1.
- Setter-only atom subscriptions → useSetAtom: space-tree-row, use-tree-mutation,
use-tree-socket no longer subscribe every visible row to the WHOLE treeDataAtom
value (a tree event re-rendered all ~20-30 rows, bypassing the DocTreeRow memo).
space-tree-node-menu / mention-list read the tree imperatively (store.get) in
their handlers only. breadcrumb.tsx uses a selectAtom slice (ancestor chain +
field equality) instead of the whole-tree subscription.
- Socket handler cleanup (BUG): use-tree-socket + use-query-subscription now
socket.off() their named handlers on cleanup (were accumulating listeners on
every reconnect → duplicated invalidations/tree-walks). Mirrors
use-notification-socket.
- Field-update tree path: invalidateOnUpdatePage does a pointwise patch of the
cached embed subtrees instead of a blanket invalidatePageTree() (refetch storm);
structural events keep the blanket invalidate.
- usePageMetaQuery: a content-less select slice for the 13 peripheral subscribers
that read only title/permissions/id, so they stop re-rendering every ~3s while
typing / on every collab page.updated (page.tsx keeps the full query for content).
- page.tsx: skeleton + placeholderData keepPreviousData (no blank flash on nav).
- Removed refetchOnMount:true where socket/mutation invalidation already keeps the
cache fresh (favorite/space/space-watcher/workspace). KEPT it on the 3 queries
with NO other freshness path (trash-list, created-by, recent-changes) — the
global default is refetchOnMount:false, so those overrides are load-bearing.
- Small: resize mousemove/up attached only while dragging; per-row emoji-picker
keydown gated on `opened`; AiChatWindow queries enabled only when the window is
open.
Gate: client tsc 0, client vitest page+websocket 200 passed (+editor suites),
build ok.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F1 (MEDIUM regression): a collapsed-but-cached branch showed STALE children on
re-expand after reload (the cache keeps children of any ever-expanded branch;
refreshOpenBranches only refreshes OPEN branches, but the fetch guard skips a branch
that has cached children). New pruneCollapsedChildren(tree, openIds) resets children
to [] (keeps hasChildren) for every node NOT in the persisted open-set, recursing
into open nodes — a once-per-mount boot effect. A pruned collapsed branch is then the
'unloaded' shape handleToggle re-fetches, so its first expand reconciles fresh (as
pre-cache). Open branches keep their children (refreshOpenBranches handles them, no
double fetch). Test: a collapsed cached branch with a stale child fetches fresh on
first expand after boot.
F2: gate the >4MB size-guard console.warn behind the writeFailureWarned once-flag
(like the quota branch) so editing a huge tree no longer re-warns every ~500ms; test
that an oversized tree is not persisted + warns exactly once.
F3: narrow the use-auth privacy comment (only tree caches are swept; other
localStorage entries remain).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On page reload the sidebar tree rendered nothing until every root page
was fetched (paginated), and children of expanded branches arrived even
later (breadcrumbs effect / socket connect) — the tree visibly jumped a
couple of seconds after load.
- treeDataAtom is now a facade over atomFamily(atomWithStorage) keyed
treeData:v1:{workspaceId}:{userId} with getOnInit: true — the cached
tree hydrates synchronously and paints on the very first render,
together with the already-persisted open-branches map. Public atom
interface unchanged (value or functional updater), all call sites
untouched.
- Custom sync storage: debounced writes (500ms, coalesced, size guard,
beforeunload flush), defensive reads (corrupted JSON -> []), no
cross-tab subscribe (localStorage is a boot cache only).
- SpaceTree renders on cached data immediately; "No pages yet" still
waits for the server. Once server roots merge, open loaded branches
are re-fetched fresh and reconciled once per space (shared
refreshOpenBranches, also used by the socket reconnect handler).
- Logout hygiene: clearPersistedTreeCaches() purges treeData:v1:* and
openTreeNodes:* by prefix and disables further persistence (kill
switch closes the websocket-write-vs-beforeunload-flush resurrection
race). Wired into both handleLogout and the 401 redirectToLogin path,
since cached trees contain page titles.
- Tests: tree-data-atom.test.ts (hydration, debounce round-trip,
corrupted JSON, scope isolation, logout purge, persistence kill
switch); expand-all suite adapted. 144 tree tests / full client suite
green, tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third tree-sync finding (#8). On a socket reconnect after a missed-events gap
(laptop sleep / wifi blip), the resync only invalidated the ROOT sidebar query;
a move/rename/delete that happened INSIDE an already-loaded, expanded branch was
never reflected — the branch stayed stale until the user manually interacted.
(The #2 fix reconciles the root level; this covers the deeper loaded branches.)
- `treeModel.reconcileChildren(tree, parentId, fresh)`: replace a loaded
branch's DIRECT children with the authoritative fresh set (drop removed, add
new, reorder to server) while PRESERVING each surviving child's already-loaded
grandchildren, so deeper expansion is not collapsed. An unloaded branch
(children === undefined) is left untouched (lazy-load fetches it fresh).
- `loadedOpenBranchIds(tree, openIds)`: the branches a reconnect should refresh
(open AND loaded). `fetchAllAncestorChildren(..., { fresh: true })` bypasses
the 30-min sidebar cache so the reconcile sees current data (handler-order
independent).
- space-tree: on socket `connect`, re-fetch + reconcile each open loaded branch
of the active space (space-switch-guarded; an unloaded branch is skipped).
Tests: reconcileChildren (drop/add/reorder + preserve grandchildren + unloaded
no-op) and loadedOpenBranchIds (open+loaded only, skip unloaded, nested). The
pure logic is unit-tested; the live socket-reconnect round-trip is not
browser-automated (simulating a reconnect gap is impractical) — sidebar render +
expand were smoke-tested with no regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Batch of fixes from the automated QA pass on develop. Each was reproduced and
then verified fixed live (browser/curl); logic-bearing fixes have unit tests.
Functional bugs:
- #122 collab-token was capped by the anonymous public-share-AI throttler (5/min);
skip all non-AUTH named throttlers on this auth-guarded, client-cached route.
- #123 editor onAuthenticationFailed threw `jwtDecode(undefined)` and never
reconnected; read the token via a ref, guard the decode (incl. missing exp),
and refetch+reconnect on any auth failure.
- #124 a slash command containing a space ("/Heading 1") inserted literal text;
enable allowSpaces and close the menu when the query matches no items.
- #125 space slug auto-gen produced uppercase initials for multi-word names;
computeSpaceSlug now yields a lowercase alphanumeric slug.
- #126 AI chat window position/size now persisted (atomWithStorage) across reload;
also fixes a latent ResizeObserver-attach bug on first open.
- #127 workspace name update accepted URLs; add @NoUrls (parity with setup).
- #132 icon-columns 4/5 passed calc() into SVG width/height attrs (console spam);
size via style. share-for-page query returns null instead of undefined.
- #134 "Reindex now" counter looked stuck: reindex runs async; the client now
polls coverage (bounded) so the counter climbs live; misleading server comment
reworded.
UX / consistency:
- #128 add success toasts to favorite/label/avatar/member-(de)activate.
- #129 "1 result found" pluralization; hide the single-option Type filter.
- #130 replace raw Zod strings with friendly messages (name/password/group).
- #131 unify "Untitled" casing in tree/breadcrumb/tab; stop force-uppercasing
space-name chips; fix confirm-dialog labels (Cancel / Remove), invite
placeholder typo, Export/Move-to-space labels.
- #133 disable profile Save when clean; toast on unsupported avatar image;
style the invalid-invitation page with a CTA; hide Share for read-only users;
align the dictation "not configured" message; "Go to login page" typo.
Tests: computeSpaceSlug, workspace-name NoUrls DTO, share-query null
normalization, slash getSuggestionItems empty-close.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ~330 tests across server (Jest), client (Vitest), editor-ext (Vitest)
and packages/mcp (node:test) for the gitmost features added since
053a9c0d: AI chat, AI agent roles, public-share assistant, MCP per-user
auth, HTML embed, page templates/embed, realtime tree, tree
expand/collapse, and the AI-settings UI.
Test-tooling fixes (prerequisite, were silently hiding coverage):
- Repair 3 page-template specs broken by the 11-arg TransclusionService
constructor; they never compiled, so template access-control / content
-leak / unsync-strip coverage was fictitious.
- Build @docmost/editor-ext before server tests via a `pretest` hook;
the stale dist omitted the new HtmlEmbed/PageEmbed exports (TS2305).
- Let jest resolve the .tsx email templates: add `tsx` to
moduleFileExtensions and widen the ts-jest transform to (t|j)sx?.
Behaviour-preserving "extract pure core" refactors that the tests drive:
- server: resolveShareAssistantRequest + uiMessageTextLength
(public-share controller), decideBasicGate + mapAuthResultToResponse
(mcp), buildErrorAssistantRecord (ai-chat), jsonbObject export (roles).
- client: render-raw-html + shouldExecute/canEdit, decide-embed-state,
page-embed picker utils, tree-socket reducers, open/close branch maps,
isEndpointConfigured/resolveKeyField; buildTreeWithChildren now treats
a permission-trimmed orphan as a root instead of crashing.
Deferred (need a test DB or HTTP harness, documented in the specs):
repo-level Postgres integration tests and the public-share XFF E2E.
Pre-existing DI/lib0-ESM suite failures are untouched and out of scope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- extract collectAllIds / collectBranchIds into tree/utils and use them in
space-tree.tsx instead of inline closures
- drop the duplicate SidebarPageTreeDto, reuse the existing SidebarPageDto
for the /pages/tree endpoint
- type the getSpaceTree client call as api.post<{ items: IPage[] }>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the denser page-tree layout opt-in instead of hardcoded, so row
density can be toggled per deployment via the COMPACT_PAGE_TREE runtime
config flag.
- doc-tree: extract ROW_HEIGHT_STANDARD (32) / ROW_HEIGHT_COMPACT (26);
default the virtualizer row stride to STANDARD density.
- client: isCompactPageTreeEnabled() in lib/config (reads
COMPACT_PAGE_TREE, default true); used by space-tree and shared-tree
to choose the row height.
- server: EnvironmentService.isCompactPageTreeEnabled() and expose
COMPACT_PAGE_TREE through the window runtime config (static.module).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a server-authoritative whole-tree endpoint and sidebar menu commands
so a deep space tree can be expanded in one request instead of a per-level
BFS storm.
Server:
- POST /pages/tree (SidebarPageTreeDto: spaceId | pageId), same CASL space
scoping as /sidebar-pages. Returns the whole space tree / subtree as a flat
list in the sidebar item shape (id, slugId, title, icon, position,
parentPageId, spaceId, hasChildren, canEdit), ordered by position
(collate C byte order), content never fetched.
- page.service.getSidebarPagesTree reproduces getSidebarPages' two-branch
permission model: open space -> spaceCanEdit; restricted space -> seed the
full descendant set then prune via filterAccessibleTreePages +
filterAccessiblePageIdsWithPermissions (keeps restricted-but-granted pages,
prunes inaccessible subtrees). hasChildren is derived from the final
filtered set so it can never reveal inaccessible children.
- page.repo.getSpaceDescendants: recursive CTE seeded by space roots.
Client:
- SpaceTree is forwardRef exposing expandAll/collapseAll/isExpanding;
expandAll fetches the whole tree once, replaces current-space nodes, opens
every branch (current space only), aborts on space switch, surfaces real
errors; collapseAll collapses only current-space ids (shared open-map).
- SpaceMenu gains Expand all / Collapse all items (no admin gate).
Implements docs/backlog/tree-expand-collapse-all.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(tree): replace react-arborist with custom tree implementation
* feat(tree): keyboard arrow navigation between rows
* feat(emoji-picker): focus search input on open
* refactor(emoji): switch to @slidoapp/emoji-mart fork for accessibility
* feat(tree): Home/End and typeahead keyboard navigation
* feat(tree): roving tabindex and * to expand sibling subtrees
* feat(tree): Space activation and ARIA refinements
* fix(tree): move treeitem role to focusable row + aria-current
* init
* navigate in overview if current page is in deleted node
* fix: implement pagination in sidebar-pages queries
* fix: appendNodeChildren()
Preserve deeper children if they exist and remove node if deleted
* feat: Move the page to another space
- The ability to move a page to another space has been added
* feat: Move the page to another space
* feat: Move the page to another space
- Correction of the visibility attribute of elements that extend beyond the boundaries of the space selection modal window
* feat: Move the page to another space
- Added removal of query keys when moving pages
* feat: Move the page to another space
- Fix locales
* feat: Move the page to another space
* feat: Move the page to another space
- Fix docker compose
* feat: Move the page to another space
* feat: Move the page to another space
- Some refactor
* feat: Move the page to another space
- Attachments update
* feat: Move the page to another space
- The function of searching for attachments by page ID and updating attachments has been combined
* feat: Move the page to another space
- Fix variable name
* feat: Move the page to another space
- Move current space to parameter of component SpaceSelectionModal
* refactor ui
---------
Co-authored-by: plekhanov <astecom@mail.ru>
* delete unused component
* return page prosemirror content
* prefetch pages
* use prosemirro json content on editor
* cache page query with id and slug as key
* Show notice on collaboration disconnection
* enable scroll while typing
* enable immediatelyRender
* avoid image break in PDF print
* Comment editor rendering props
* feat: support i18n
* feat: wip support i18n
* feat: complete space translation
* feat: complete page translation
* feat: update space translation
* feat: update workspace translation
* feat: update group translation
* feat: update workspace translation
* feat: update page translation
* feat: update user translation
* chore: update pnpm-lock
* feat: add query translation
* refactor: merge to single file
* chore: remove necessary code
* feat: save language to BE
* fix: only load current language
* feat: save language to locale column
* fix: cleanups
* add language menu to preferences page
* new translations
* translate editor
* Translate editor placeholders
* translate space selection component
---------
Co-authored-by: Philip Okugbe <phil@docmost.com>
Co-authored-by: Philip Okugbe <16838612+Philipinho@users.noreply.github.com>
* wip
* Space export
* option to export pages with children
* include attachments in exports
* unified export UI
* cleanup
* fix: change export icon
* add export button to space settings
* cleanups
* export name
* Maintain tree open state on route change and return
* Load page tree ancestors and their children when a page is accessed directly
* Show correct breadcrumb path
* Add emoji to breadcrumbs
* Backend api to get page breadcrumbs/ancestors