Skip to content

feat(groups): support nested connection groups with cascade delete#405

Open
p4pupro wants to merge 10 commits into
TabularisDB:mainfrom
p4pupro:pr/nested-groups
Open

feat(groups): support nested connection groups with cascade delete#405
p4pupro wants to merge 10 commits into
TabularisDB:mainfrom
p4pupro:pr/nested-groups

Conversation

@p4pupro

@p4pupro p4pupro commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Connection groups can now nest to N levels. New sub-folders are created from
a visible + button on each group header or by typing a /-separated path
in the toolbar ("New group" → clients/eu/finance creates the missing
segments). Deleting a group removes its entire subtree of sub-folders and
connections, and the export/import round-trip preserves the hierarchy.

What changed

  • ConnectionGroup already carried parent_id; this wires it up end-to-end
    in both the renderer and the find_connection_by_id /
    get_connections_with_groups flows so sub-folders render in list and grid
    view with a per-level indent.
  • New Tauri command create_group_path(path, parent_id?) that walks the path
    segment by segment, reusing existing groups case-insensitively and
    creating the missing ones under the resolved parent. Sibling lookups are
    scoped to the current parent so /a/b and /x/b are distinct.
  • A pure collect_group_subtree(groups, root_id) helper computes the set of
    group IDs to delete (root + all transitive descendants). delete_connection_group
    now drops every group in that set plus every connection whose group_id
    is in the set. The frontend re-fetches via get_connections_with_groups
    after the call so the optimistic state is never responsible for the cascade.
  • Export/import now writes and reads parent_id per group, so a connection
    bundle exported with nested folders re-imports with the same tree.
  • The existing ensure_no_cycle check still runs on create/update, so the
    hierarchy can never become a cycle even if a user tries to drag a folder
    into its own descendant.

UI

  • Visible + button on every group header (replaces the old right-click-only
    path). Hover reveals an amber plus that focuses an inline input rendered
    between the header and its children; Enter confirms, Escape cancels.
  • Toolbar "New group" input accepts /-separated paths: typing
    clients/eu/finance creates the missing segments and returns the deepest
    group so the caller can keep typing.
  • Indent in both the list and grid view: sub-folders and their connections
    are pushed right one level per ancestor.
  • Group delete is now destructive: a single click on the existing trash
    action removes the whole subtree (groups + connections) with a confirm
    prompt that names how many children will go.

Nested groups with the + button and indent

Image showing the related UI state

Cascade delete confirmation

Tests

  • collect_group_subtree is covered for: leaf only, full descendant chain,
    subgroup not including siblings, and unknown id (returns singleton).
  • cascade_delete_removes_parent_descendants_and_connections and
    cascade_delete_subgroup_leaves_parent_and_other_subgroups_alone exercise
    the Tauri command end-to-end against an in-memory store.
  • group_tree_tests.rs covers the cycle prevention and the path-walking
    helpers (parse_group_path, find_child_group).
  • export_import_tests.rs round-trips a three-level tree and asserts
    parent_id survives the JSON dance.

cargo build --release, cargo test --lib (767 passing; the four askpass
failures are pre-existing on main), and pnpm tsc --noEmit all pass.

i18n

Added subgroupNamePrompt under groups and translated the new useIamAuth
strings in all eight locales (en, es, de, fr, it, ja, ru, zh). en and it
didn't have a groups namespace before, so it was created.

domingo-perez-frs-systems and others added 10 commits July 1, 2026 10:05
Replaces the previous single-level group hierarchy with arbitrary-depth
nesting via a new optional `parent_id` field on `ConnectionGroup`.
Groups can now contain other groups (subfolders), which can themselves
contain groups, and so on.

Backend
- models: `ConnectionGroup` gains `parent_id: Option<String>` with
  `#[serde(default, skip_serializing_if = "Option::is_none")]` so
  pre-existing `connections.json` files keep working unchanged.
- commands: `create_connection_group` accepts an optional
  `parent_id`, validates that the parent exists, and computes
  `sort_order` per-parent (siblings are appended to the end of their
  own chain rather than the global maximum).
- commands: new `move_group_to_parent` Tauri command re-parents an
  existing group. Self-loops and missing parents are rejected with
  actionable errors. A new `reject_if_would_create_cycle` helper
  walks the parent chain of the target to ensure no cycle is
  introduced; the walk is bounded to fail-safe against pre-existing
  corruption in `connections.json`.
- commands: `delete_connection_group` now re-parents direct children
  to the deleted group's parent before removing it, so dropping a
  folder never silently orphans its contents.
- lib: registers the new command in the invoke handler.
- tests: 7 new unit tests in `group_tree_tests` cover the cycle
  detector (none parent, same id, direct parent, deep descendant,
  unrelated target, pre-existing corruption, target not in tree).

Frontend
- types: `ConnectionGroup` gains `parent_id?: string | null`;
  `createGroup` signature is widened to accept an optional
  `parentId`. New `moveGroupToParent` action added to the context.
- DatabaseProvider: passes the parent id through to the backend and
  optimistically re-parents direct children on delete.
- Connections.tsx: `sortedGroups` is replaced with a
  `groupsByParent` map; a new `renderGroupTree` function walks the
  tree recursively for both grid and list views. Indent is capped at
  6 levels (16px per level) to avoid runaway visual nesting.
- Drag-and-drop on a group header now has two modes: dropping near
  the left edge keeps the existing top-level reorder behavior;
  dropping past one indent step re-parents the source under the
  target. Cycle attempts surface a clear error.
- A 'New subfolder' entry is added to the group context menu, which
  prompts for a name and creates the subgroup via the new
  `createGroup(name, parentId)` path.
- The header connection-count badge now sums direct + descendant
  connections so users can see at-a-glance how many items live in
  a folder tree.

Backwards compatibility
- The optional `parent_id` defaults to `None`, so the change is
  transparent for users who never create subgroups.
- All existing top-level groups render exactly as before when
  `parent_id` is null.

Tests: cargo 682/682, vitest 2584/2584, tsc clean.
The code is sufficiently self-explanatory after the comments are removed.
The recursive renderer, cycle detector, and per-parent sort order speak
for themselves once the surrounding prose is gone.
Adds two discoverability improvements to the nested-group workflow:

1. A visible '+' button on each group header (replacing the only
   context-menu path that was previously hidden behind a right-click).
   Hovering a group reveals an amber plus button that focuses an
   inline input rendered between the header and the group's
   children, with Enter to confirm and Escape to cancel.

2. '/' as a hierarchical path separator in both the toolbar 'New
   group' input and the inline 'New subfolder' input. Entering
   'TEST/flexways' now:
     - reuses an existing 'TEST' group case-insensitively if found;
     - creates the missing segments under it (here 'flexways');
     - returns the deepest group so the caller can keep typing.

Backend

  - new Tauri command create_group_path(path, parent_id?) that walks
    the path segment-by-segment, calling the existing
    create_connection_group for any segment that does not already
    exist under the resolved parent. Sibling lookups are scoped to
    the current parent only, so '/a/b' and '/x/b' are different.
  - helpers parse_group_path (splits on '/', trims whitespace, drops
    empty segments, rejects an empty result) and find_child_group
    (case-insensitive, parent-scoped lookup).
  - 6 new unit tests in group_tree_tests covering the parser and the
    case-insensitive scoped lookup.

Frontend

  - GroupHeader gains an optional onCreateSubgroup prop and renders
    the new Plus button before the MoreVertical menu trigger.
  - DatabaseContext + DatabaseProvider expose createGroupPath which
    invokes the new command and re-fetches get_connection_groups so
    the UI reflects reused + newly created segments in one render.
  - Connections.tsx wires the inline subgroup input and replaces
    the toolbar 'New group' handler with createGroupPath so the
    same path semantics apply at the top level.

i18n

  - new key 'groups.subgroupNamePrompt' in en, de, fr, ja, ru, zh.
    es and it were already missing the 'groups' namespace after the
    previous nested-groups PR, so they keep falling back to the
    defaultValue.
After PR #1/#3, subfolders are technically rendered inside their
parent's container, but the group header itself was rendered flush
to the left, at the same horizontal position as the parent group's
header. Only the connection cards were indented, which made the
hierarchy hard to read at a glance: a folder called 'flexways' sat
exactly under 'TEST' visually, and you had to look at the cards
below to confirm it was a child.

Pass the existing render-tree depth through to GroupHeader as a new
optional 'depth' prop. GroupHeader uses it to push the whole header
20px to the right per nesting level (capped at 6 levels = 120px,
matching the existing connection indent cap). The connection
container below gets an extra 20px per level on top of its previous
indent so cards stay visually anchored to their header rather than
to the parent's left edge.

Pure presentation change: no data model, context, or backend
touched. Works identically in list and grid view since both share
the same renderGroupTree recursion. Validated with pnpm tsc --noEmit
(clean) and visually in pnpm tauri dev.
The export side already worked: 'export_connections_payload' returns
conn_file.groups verbatim, which includes every ConnectionGroup with
its parent_id set. A roundtrip through JSON preserves the field
because it is serialized on the struct. Tested directly.

The import side had a latent bug: it accepted any parent_id without
verifying that the referenced group actually exists in the union
(incoming payload + local config). A JSON with a malformed
parent_id, or a payload that only carries a subtree of a larger
tree, would land dangling references in the local config. The UI
then renders the orphan as if it were a child, but the parent's
card never appears, so it looks like a flat list.

The fix moves the inline merge loop into a small 'merge_groups'
helper that:
  1. inserts / overwrites groups by id (existing semantics);
  2. after the merge, walks the resulting list and demotes any
     parent_id that doesn't resolve to a known id back to None
     (root). This is tolerant: a partially-malformed JSON still
     imports successfully, and the user keeps most of their tree.

merge_groups is pub(crate) so the export/import test module can
call it directly. ConnectionGroup gains PartialEq + Eq to support
the idempotency assertion.

Tests added (export_import_tests):
  - test_export_preserves_nested_group_hierarchy
  - test_merge_groups_imports_full_subtree_preserving_hierarchy
  - test_merge_groups_demotes_orphaned_parent_id_to_root
  - test_merge_groups_keeps_existing_parent_when_payload_overrides
  - test_merge_groups_is_idempotent
  - test_merge_groups_incoming_parent_in_existing_only

cargo test --lib: 710/710 pass (was 704).
Deleting a connection group now removes the target group, every nested
child group (transitively), and every connection belonging to any group
in that subtree. Previously direct children were re-parented to the
grandparent and connections in the deleted group became ungrouped, which
left orphaned subtrees when a top-level group was removed.

A pure helper `collect_group_subtree(groups, root_id)` is extracted
into models so the cascade logic is testable without an AppHandle. Six
new unit tests cover leaf / deep chain / sibling isolation / unknown
id / parent delete / subgroup delete.

Frontend: `deleteGroup` re-fetches `get_connections_with_groups` so
the optimistic state mirrors the persisted file instead of re-implementing
the cascade in the client.
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.

2 participants