Skip to content

feat(sidebar): add folders for tables and views - #3189

Merged
datlechin merged 3 commits into
mainfrom
feat/table-folders
Sep 30, 2026
Merged

datlechin merged 3 commits into
mainfrom
feat/table-folders

Conversation

@datlechin

@datlechin datlechin commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Fixes #3167

Folders for tables and views in the sidebar's object list, the way saved queries already have folders. Folders work on each Mac now. Syncing them between Macs is built but stays off until the two CloudKit record types are deployed to Production (see Turning on sync), so the CHANGELOG and the docs do not claim sync yet: the iCloud Sync page lists Table Folders as Not yet, the same as Credential Profiles.

What it does

  • Where folders live. In Sidebar as List, a Folders section sits between Recent and Tables. In Sidebar as Tree, each database or schema lists its own folders above its kind groups. A folder holds tables and views of one database or schema, one level deep. A filed object moves out of its Tables or Views section into the folder.
  • Making and filing. Right-click a table and choose Move to > New Folder, or right-click Tables, Views, the Folders header or the empty area and choose New Folder. The new folder opens with its name selected in the row, like Finder. Move to lists the other folders; Remove from Folder takes an object out. Tables and views also drag onto a folder, or back onto their section.
  • Folder rows. Rename, New Folder and Delete Folder. Deleting puts the objects back in their sections without an alert, and Cmd+Z restores it. Every folder edit is on the connection's undo stack.
  • Search. The filter reaches inside folders: a folder holding a match opens, one without is hidden.
  • Catalog changes. The store is a TableScopedSettingsStore, so renaming or dropping a table, schema or database from the sidebar, and deleting the connection, carry its folders along the same way per-table settings go.

Sync

Two new record types, TableFolder and TableFolderItem (one record per filed object, named by an escaped hash of connection, database, schema and name), with a Table Folders toggle in Settings > Sync. Both are declared unverified, so CloudKitSyncEngine withholds them until the schema is in Production.

  • Only folders of a connection this Mac syncs go up: not Local only, not the sample, and nothing when the connection store cannot be read. A Local only connection also takes no folder change from another device, matching fix(connections): keep Local only connections out of remote edits and deletions #3178.
  • A pulled folder or placement loses to a local change that has not been pushed yet, deletions included, so an Undo made while a deletion was on its way up is not deleted again by that deletion's echo.
  • The first run of a build that syncs record types the last one did not clears the change token once, so folders pushed by an upgraded Mac reach a Mac that upgrades later. It counts only types verified in Production, so it does nothing until the flip.

Fixed along the way

These are shared code, and folders make each of them routine:

  • A rename field closed by any reload. A reload removed the field that had focus, AppKit ended editing, and the half-typed name was committed. That is what closed a new folder's name field the moment the store's own change notification reloaded the tree. The outline now holds reloads while a rename or a drag is in flight, the rule the Welcome list already follows, and applies them after.
  • A record deleted and re-created before a push. SyncChangeTracker.markDirty left the pending tombstone in place, so the next push sent a save and a deletion for the same record. It now withdraws it.
  • A deletion of a record the server never had. A folder created and deleted between two syncs made every later sync fail on unknownItem. macOS now settles those the way the iPhone coordinator already did.

Turning on sync

  1. Add TableFolder and TableFolderItem to the Development schema (Console, or xcrun cktool import-schema --environment development with the current schema plus the two blocks below), then deploy Development to Production.
  2. Run scripts/export-cloudkit-schema.sh and commit CloudKit/production-schema.ckdb.
  3. Fill TableFolderSyncField.verifiedInProduction, TableFolderItemSyncField.verifiedInProduction and SyncRecordType.verifiedInProduction, and empty pendingProductionDeploy in SyncSchemaGateTests. SyncTableFolderTests.recordsRoundTrip switches itself on at that point.
  4. Change the Table Folders row in docs/features/icloud-sync.mdx to Yes and add a CHANGELOG entry for folder sync.
RECORD TYPE TableFolder (
    "___createTime" TIMESTAMP, "___createdBy" REFERENCE, "___etag" STRING,
    "___modTime" TIMESTAMP, "___modifiedBy" REFERENCE, "___recordID" REFERENCE,
    connectionId STRING QUERYABLE SEARCHABLE SORTABLE,
    createdAt TIMESTAMP QUERYABLE SORTABLE,
    database STRING QUERYABLE SEARCHABLE SORTABLE,
    folderId STRING QUERYABLE SEARCHABLE SORTABLE,
    modifiedAtLocal TIMESTAMP QUERYABLE SORTABLE,
    name STRING QUERYABLE SEARCHABLE SORTABLE,
    schema STRING QUERYABLE SEARCHABLE SORTABLE,
    schemaVersion INT64 QUERYABLE SORTABLE,
    updatedAt TIMESTAMP QUERYABLE SORTABLE,
    GRANT WRITE TO "_creator", GRANT CREATE TO "_icloud", GRANT READ TO "_world"
);

RECORD TYPE TableFolderItem (
    "___createTime" TIMESTAMP, "___createdBy" REFERENCE, "___etag" STRING,
    "___modTime" TIMESTAMP, "___modifiedBy" REFERENCE, "___recordID" REFERENCE,
    connectionId STRING QUERYABLE SEARCHABLE SORTABLE,
    database STRING QUERYABLE SEARCHABLE SORTABLE,
    folderId STRING QUERYABLE SEARCHABLE SORTABLE,
    modifiedAtLocal TIMESTAMP QUERYABLE SORTABLE,
    name STRING QUERYABLE SEARCHABLE SORTABLE,
    schema STRING QUERYABLE SEARCHABLE SORTABLE,
    schemaVersion INT64 QUERYABLE SORTABLE,
    GRANT WRITE TO "_creator", GRANT CREATE TO "_icloud", GRANT READ TO "_world"
);

Merged with main

#3179 replaced the per-category checks on the pull and on remote deletions with one exhaustive SyncSettings.syncs(_:). The folder arms now sit under that guard, and .tableFolder and .tableFolderItem map to syncTableFolders in it.

Tests

  • Unit:
    • New: TableFolderStorageTests, TableFolderPlannerTests, TableFolderDropResolverTests, DatabaseTreeFolderNodeTests, DatabaseTreeReloadHoldTests, SyncTableFolderTests.
    • Extended: DatabaseTreeMenuSpecTests, SyncCoordinatorEchoTests, SyncChangeTrackerTests.
    • DatabaseTreeReloadHoldTests covers the rename-field fix. Its first form failed before the fix and passed after.
  • Package: SyncMetadataStorageTests and SyncRecordTypeTests in TableProSyncTests.
  • UI: TableFolderUITests covers Move to > New Folder, naming the folder in its row, Delete Folder, and Cmd+Z bringing it back.
    • The sample database is SQLite in the flat layout, so the tree layout is covered by the unit suites instead.
    • Drag and drop is covered by TableFolderDropResolverTests and not driven through XCUITest.

Review

  • Codex, working tree. Fixed: a record re-created before a push kept its tombstone, filed-only kind sections vanished as drop targets, and the Folders section stayed closed after Move to. The schema gate is by design, per Before merge.

  • Codex, adversarial. Fixed:

    • undo moving a folder back to a renamed schema;
    • an unreadable folder document being overwritten;
    • the Local only filter failing open when connections cannot be read;
    • folders paired with the wrong catalog during a database switch;
    • two placements sharing one key after a schema rename;
    • deletions of records the server never had;
    • a pull overwriting an unpushed edit;
    • folders never reaching a Mac that upgrades later.

    Not fixed here: turning a sync category off and on never replays what it skipped. That holds for every category, not only folders.

  • Codex, branch against main. Fixed: Move to hid the right-clicked row's folder for a selection spread over several folders. Not changed:

    • Holding the pull token when a remote folder change cannot be written. An unreadable document stays unwritable until repaired, and holding the token would stall every pull for every record type.
    • Keeping tombstones of a Local only connection's folders off iCloud. Database favorites already push them, by the stated policy that a deletion only removes something.
    • Re-marking a connection's folders when it leaves Local only. The same gap exists for table and database favorites; it wants one fix for all of them.
  • Security review. No findings.

  • Simplify pass.

    • Applied: DatabaseScope instead of a duplicate scope type, the key through PreferenceKeys, one commit path in the store, a per-kind planner, member rows carried on the folder row, one exhaustive TableFolderCommand, drag items resolved once per drag, and held reloads coalesced.
    • Skipped: moving the missing-deletion settle into CloudKitSyncEngine, since the transport protocol is where the tests script outcomes; and folding folder plans into objectBuckets, since search needs filed objects counted as matches.

Screenshot

A new screen, so there is no before.

Sidebar with a Folders section holding Music and Sales above the Tables section, light appearance

The same sidebar in dark appearance

@mintlify

mintlify Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 29, 2026, 1:23 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

# Conflicts:
#	CHANGELOG.md
#	TablePro/Core/Sync/Extensions/SyncCoordinator+RemoteDeletions.swift
#	TablePro/Core/Sync/SyncCoordinator.swift
@datlechin
datlechin marked this pull request as ready for review September 30, 2026 12:22
@datlechin
datlechin merged commit 3cf419c into main Sep 30, 2026
6 of 8 checks passed

This branch was successfully deployed

1 active deployment
staging - docs — a72b9caa Deployed Sep 30, 2026 by mintlify[bot]
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.

Groups/folders for tables/views

1 participant