Skip to content

feat(connections): support multiple tags per connection with tag filtering (#744)#1753

Merged
datlechin merged 2 commits into
mainfrom
feat/multi-tag-connections
Jun 22, 2026
Merged

feat(connections): support multiple tags per connection with tag filtering (#744)#1753
datlechin merged 2 commits into
mainfrom
feat/multi-tag-connections

Conversation

@datlechin

Copy link
Copy Markdown
Member

Closes #744.

Problem

Connections could hold at most one tag (DatabaseConnection.tagId: UUID?), so tags behaved like mutually exclusive groups. There was no way to assign several tags or filter by them.

Change

Tags are now a [UUID] set per connection, end to end:

  • Model: tagIdtagIds with a one-way Codable migration that promotes an existing single tag into the array.
  • Connection form: the Tags row is a multi-select menu (checkmarked list of all tags, create-with-color, delete) that updates live.
  • Welcome list: a tag filter bar with Match Any / Match All; per-row colored tag dots.
  • Toolbar: shows the connection's first tag as a badge with a +N overflow that opens the full list.
  • Storage / sync / export: StoredConnection, CloudKit SyncRecordMapper (macOS + iOS), ExportableConnection, and the tablepro:// deep link all carry multiple tags, dual-writing the first tag for backward compatibility with older app versions.
  • iOS: model + sync parity (tagId kept as a computed { tagIds.first }); no iOS multi-tag UI in this change.

Tests

Added unit tests for the Codable migration (tagId → tagIds, encode drops the legacy key), StoredConnection round-trip + back-compat dual-write, SyncRecordMapper round-trip + legacy fallback, and TagFilter match-any/all + tree filtering. Updated existing import/sharing tests for the new init signatures.

Notes

  • Deleting a tag now clears it from every connection before the sync tombstone fires (respects the sync delete-ordering invariant; also fixes a latent macOS dangling-reference bug).
  • SwiftLint --strict clean. CHANGELOG and the URL-scheme doc updated.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c675898213

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

sslEnabled: type == .mssql ? (mssqlSSLMode != .disable) : sslEnabled,
groupId: groupId,
tagId: tagId
tagIds: tagId.map { [$0] } ?? []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve all tag IDs when saving on iOS

When a connection arrives from sync/import with more than one tag, ConnectionFormViewModel.init still stores only conn.tagId (the first tag) in the single tagId field. Saving any edit then rebuilds the model here as tagIds: tagId.map { [$0] } ?? [], so all secondary tags are silently removed and can be synced/exported as a deletion even if the user did not touch tags. Keep existingConnection.tagIds when the tag field is unchanged, or make the iOS form write the full tag array.

Useful? React with 👍 / 👎.

sslConfig: sslConfig,
color: value("color"),
tagName: value("tagName"),
tagNames: values("tagName").isEmpty ? nil : values("tagName"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Create every tag from repeated deeplink parameters

For a deeplink such as tablepro://import?...&tagName=prod&tagName=readonly on a device where readonly does not already exist, this captures both names in tagNames, but DeeplinkImportSheet still builds the import envelope with tags: connection.tagName.map { ... }, which includes only the first tag. performImport only creates tags listed in that envelope, so buildDatabaseConnection cannot resolve the second name and the imported connection loses it unless the tag was pre-existing.

Useful? React with 👍 / 👎.

@datlechin datlechin merged commit 9d8c26b into main Jun 22, 2026
2 of 3 checks passed
@datlechin datlechin deleted the feat/multi-tag-connections branch June 22, 2026 17:01

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80b268378c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +86 to 90
if !connection.tagIds.isEmpty {
let tagIdStrings = connection.tagIds.map { $0.uuidString }
record["tagIds"] = tagIdStrings as CKRecordValue
record["tagId"] = tagIdStrings[0] as CKRecordValue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear tag fields when the last tag is removed

When a connection loses its last tag, this branch writes neither the new tagIds field nor the legacy tagId field. The macOS push path builds a fresh record here and saves it via CloudKitSyncEngine with .changedKeys, which the local wrapper documents as overwriting only fields that are set, so iCloud can retain the stale tag values and other devices can resurrect the deleted association. Use an update path that includes field deletions or otherwise explicitly clear both tag fields for the empty-array case.

Useful? React with 👍 / 👎.

Comment on lines +226 to +228
if !vm.availableTags.isEmpty {
TagFilterBar(tagFilter: $vm.tagFilter, availableTags: vm.availableTags)
Divider()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the active tag filter visible

If the active filter is the only tag in use and the last matching connection has that tag removed or deleted, vm.availableTags becomes empty while vm.tagFilter remains active. This guard then hides TagFilterBar, including its Clear button, so the list stays filtered to an empty state with no visible way to recover. Show the bar whenever a filter is active, or clear selected IDs that are no longer available.

Useful? React with 👍 / 👎.

Comment on lines +135 to +137
if !tagIds.isEmpty {
try container.encode(tagIds, forKey: .tagIds)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Dual-write the legacy tagId for mobile persistence

TableProMobile persists [DatabaseConnection] by JSON-encoding this public model, but tagged connections now serialize only tagIds. The previous mobile model decoded only tagId, so opening the same on-disk data with an older build drops even the first tag, unlike the macOS stored model and sync/export paths that dual-write the legacy field for compatibility. Encode tagIds[0] under .tagId here when the array is non-empty.

Useful? React with 👍 / 👎.

@mintlify

mintlify Bot commented Jun 22, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated (UTC)
TablePro 🔴 Failed Jun 22, 2026, 5:18 PM

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

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.

Tags behave like groups instead of true multi-tags (cannot assign multiple tags)

1 participant