Skip to content

Add the collectionTreeRoots extension point to the sidebar collection tree #3314

Closed

Description

@mstomar125

Summary

Add the collection-tree roots extension point from ADR-082 §2.2 (proposed):

  • An extension names collections that the sidebar tree treats as invisible containers.
  • Core unions the roots from every extension with its own current root.
  • With no extension registered, the tree is unchanged.

This lets nodespace-sync contribute the Pro workspace root before #3330 deletes core's own root. #3330's SYNC gate can therefore be met before #3330 merges.

Context

src/lib/stores/collections.svelte.ts:

  • L6 imports databaseStore.
  • The collectionsTree getter (L147-170) hides databaseStore.activeDatabase?.boundTenantCollection || ROOT_COLLECTION_ID, which it passes as the 4th argument to buildCollectionsTree.
  • ROOT_COLLECTION_ID (L564-578) is the legacy workspace root.
  • buildCollectionsTree (L580-660):
    • its 4th parameter is rootCollectionId: string = ROOT_COLLECTION_ID;
    • it drops that id as a display parent (L623) and as a top-level row (L651-652).

src/tests/stores/collections.test.ts:

  • L10-11 import buildCollectionsTree and ROOT_COLLECTION_ID.
  • The describe 'buildCollectionsTree dynamic bound-root filtering' (L638) passes a string 4th argument at L658, L677, L700 and L740, and relies on the default at L721.
  • collectionsData._setTestData drives collectionsTree.

ADR rules:

  • ADR-082 §2.2: a root is a collection the tree does not show. A collection whose only parent is a root is shown at the top level. The host takes the union of every extension's list.
  • ADR-082 §2.4: a collectionTreeRoots() that throws is treated as empty and logged once.
  • ADR-083 §2 moves the Pro root out of core (Remove the tenant-binding root fallback from the collections tree #3330).

Scope

1. Contract and registry, in ui-extensions.ts

Contract. Add NodespaceExtension.collectionTreeRoots?: () => readonly string[]. Its doc says:

  • it is presentation only;
  • it is evaluated inside a derivation, so it must read only reactive sources and have no side effects (ADR-049).

Registry method. Add collectionTreeRoots(): ReadonlySet<string>:

  • It returns the union of every extension's roots, in registration order.
  • A contributor that throws counts as empty. It is logged with log.warn once, and not again until it next returns normally.
  • Non-string entries are ignored.

Why the plain registry, not the reactive wrapper:

2. collections.svelte.ts

  • Change buildCollectionsTree to:

    buildCollectionsTree(
      collections,
      locallyCreatedIds,
      pendingIds,
      rootCollectionIds: ReadonlySet<string> = new Set([ROOT_COLLECTION_ID])
    )
    • Filter parents with !rootCollectionIds.has(p).
    • Filter top-level rows with !rootCollectionIds.has(c.id).
    • Update the doc.
  • In the collectionsTree getter, pass new Set([boundRoot || ROOT_COLLECTION_ID, ...uiExtensionRegistry.collectionTreeRoots()]). Add a comment: extension roots are unioned with core's current root, and core's root leaves with ADR-083 §2.

  • Keep ROOT_COLLECTION_ID, the boundTenantCollection read and the databaseStore import. Remove the tenant-binding root fallback from the collections tree #3330 removes them, and makes the default set empty.

3. Fixture

Add a collectionTreeRoots option: createTestExtension({ collectionTreeRoots }).

4. Pro-boundary ratchet

Paste the printed BASELINES. No increase is expected.

Out of scope

Steps

  1. Start up as core's CLAUDE.md describes: a worktree named issue-<N>-collection-tree-roots, and self-assign at pickup.
  2. Add the registry method, with its tests.
  3. Change the buildCollectionsTree signature and the collectionsTree getter.
  4. Update and extend collections.test.ts. Then run the full frontend suite, because the store's import graph changes.

Acceptance criteria

Registry:

  • uiExtensionRegistry.collectionTreeRoots() returns the union of every extension's roots.
  • It returns an empty set when no extension contributes.
  • It survives a throwing contributor, and logs the throw once.

The tree:

  • buildCollectionsTree hides every id in its root set. Its default set is {ROOT_COLLECTION_ID}, so existing callers are unchanged.
  • collectionsData.collectionsTree hides both core's current root and every extension root.
  • With no extension registered, the tree is unchanged.

Checks:

  • bun run test (the full frontend suite), bun run test:changed and test:free-users pass, and quality:fix is clean.
  • bun run scripts/check-pro-boundary.ts exits 0, and BASELINES equal the printed counts.

Tests and verification

Changes to collections.test.ts:

  • The four explicit 4th arguments become new Set([...]).
  • New cases for buildCollectionsTree:
    • when the set holds two roots, both are hidden;
    • with an empty set, a container shows as an ordinary collection, with its member collections nested under it.
  • A new describe, 'collectionsTree unions extension collection-tree roots'. With the fixture contributing ['ext-root']:
    • ext-root is hidden, and its members are shown at the top level;
    • core's root stays hidden;
    • a throwing contributor leaves the tree intact;
    • unregistering the fixture restores the previous tree.

Changes to src/tests/plugins/ui-extensions.test.ts: union order, throw safety, and the empty case.

Commands:

bun run --cwd packages/desktop-app test src/tests/stores/collections.test.ts src/tests/plugins
bun run test
bun run test:changed
bun run --cwd packages/desktop-app quality:fix
bun run scripts/check-pro-boundary.ts

Dependencies

Risks and rollback

  • Import graph. The store gains an import of ui-extensions.ts, which itself imports only the logger.
  • Rollback. Revert the PR.

Plan key: C3.7 (pro-boundary DAG).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Labels

pro-boundaryPro/core boundary: Pro code moves to nodespace-sync (ADR-081..085)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions