Skip to content

feat(folders): soft-delete folders and show in Recently Deleted#4001

Merged
waleedlatif1 merged 12 commits intostagingfrom
waleedlatif1/sidebar-folders
Apr 7, 2026
Merged

feat(folders): soft-delete folders and show in Recently Deleted#4001
waleedlatif1 merged 12 commits intostagingfrom
waleedlatif1/sidebar-folders

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

@waleedlatif1 waleedlatif1 commented Apr 7, 2026

Summary

  • Folders are now soft-deleted (archived) instead of permanently removed, matching the existing pattern for workflows, tables, and knowledge bases
  • Users can restore deleted folders (and all their workflows) from Settings > Recently Deleted
  • Delete confirmation dialogs now show "You can restore it from Recently Deleted" for folders and workflows, with muted styling for "This action cannot be undone" where it remains

Changes

  • Schema: Add archivedAt column + index to workflowFolder table
  • Backend: Soft-delete via archivedAt timestamp instead of hard-delete; scope filter on GET /api/folders; new POST /api/folders/[id]/restore endpoint with batch workflow restoration
  • Frontend: Folders tab in Recently Deleted page; updated delete modal messaging; useRestoreFolder mutation hook
  • Styling: Changed "This action cannot be undone" from error-red to muted text across all confirmation dialogs

Test plan

  • Delete a folder with workflows → confirm it disappears from sidebar
  • Navigate to Settings > Recently Deleted > Folders → confirm folder appears
  • Restore folder → confirm it reappears in sidebar with its workflows
  • Delete a nested folder tree → confirm all levels appear in Recently Deleted
  • Restore a child folder whose parent is still deleted → confirm it appears at root
  • Verify delete modal shows restoration messaging for folders, workflows, and mixed selections
  • Verify "This action cannot be undone" uses muted text styling in all dialogs

Folders are now soft-deleted (archived) instead of permanently removed,
matching the existing pattern for workflows, tables, and knowledge bases.
Users can restore folders from Settings > Recently Deleted.

- Add `archivedAt` column to `workflowFolder` schema with index
- Change folder deletion to set `archivedAt` instead of hard-delete
- Add folder restore endpoint (POST /api/folders/[id]/restore)
- Batch-restore all workflows inside restored folders in one transaction
- Add scope filter to GET /api/folders (active/archived)
- Add Folders tab to Recently Deleted settings page
- Update delete modal messaging for restorable items
- Change "This action cannot be undone" styling to muted text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Apr 7, 2026 2:50am

Request Review

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 7, 2026

PR Summary

Medium Risk
Introduces new folder archival/restoration paths that touch DB schema, recursive lifecycle logic, and workflow restore behavior; risks are mainly around data consistency (archivedAt scoping, parent/child restoration, and cache invalidation).

Overview
Folders are now soft-deleted (archived) instead of hard-deleted, backed by a new workflow_folder.archived_at column + index, and folder listing now supports an active vs archived scope filter.

Adds end-to-end folder restore support: a new POST /api/folders/[id]/restore endpoint with permission checks, PostHog + audit logging, and transactional recursive restoration of folders and only the workflows archived with the same timestamp (with parent re-rooting when needed).

Updates the UI and client queries so Recently Deleted can show/restore folders, adjusts folder query keys/invalidation for scoped lists, and tweaks confirmation dialogs to reflect archiving/restorability (with muted styling for "This action cannot be undone").

Reviewed by Cursor Bugbot for commit a80cc8b. Configure here.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 7, 2026

Greptile Summary

This PR adds soft-delete (archive/restore) support for workflow folders, matching the existing pattern already in place for workflows, tables, knowledge bases, and files. Deleted folders and their contents are now moved to an archivedAt-gated state rather than hard-deleted, and appear under Settings > Recently Deleted where they can be restored in one click.

Key implementation highlights:

  • Shared-timestamp batch delete: deleteFolderRecursively threads a single Date object through all recursive archive operations, so every folder row and every workflow in the batch receives an identical archivedAt value.
  • Exact-timestamp restore scoping: restoreFolderRecursively uses eq(workflow.archivedAt, folderArchivedAt) to restore only those workflows archived as part of this specific folder deletion — workflows individually deleted before the folder are left archived.
  • Atomic transaction: The entire restore path (parent reparenting → folder unarchive → workflow / schedule / webhook / chat / form / MCP tool / A2A agent restore) is wrapped in a single db.transaction() with tx threaded through the recursive helper, so any failure rolls back completely.
  • Scope-aware query key: folderKeys.list(workspaceId, scope) adds a scope dimension so 'active' and 'archived' folder queries maintain separate cache entries while sharing a common folderKeys.lists() prefix for efficient targeted invalidation.
  • The AuditAction.FOLDER_RESTORED action and AuditResourceType.FOLDER resource type are both properly defined; the migration adds the nullable archived_at column plus a btree index safely.

Confidence Score: 5/5

Safe to merge — no P0/P1 issues found; implementation is correct and fully atomic with proper transaction wrapping and exact-timestamp restore scoping.

All remaining findings are P2 (a redundant ? modifier on the archivedAt type field). The critical bugs raised in earlier review rounds — non-atomic restore and restoration of independently-deleted workflows — are both fully resolved. The PR follows established soft-delete patterns and is consistent with the existing workflow/table implementations.

apps/sim/stores/folders/types.ts has a minor type precision issue (archivedAt?: Date | null should be archivedAt: Date | null); no files require attention for correctness or safety.

Important Files Changed

Filename Overview
apps/sim/lib/workflows/orchestration/folder-lifecycle.ts Core soft-delete/restore logic: shared timestamp ensures exact-match restore scoping; full db.transaction() wraps restore atomically with tx threaded through recursion
apps/sim/app/api/folders/route.ts Added scope query param with isNull/isNotNull filter; correct auth and permission checks preserved throughout
apps/sim/app/api/folders/[id]/restore/route.ts New restore endpoint with proper auth, write-permission guard, and clean delegation to performRestoreFolder
apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx Folders tab integrated cleanly alongside existing resource types; restore triggers correct cache invalidation of both active and archived scopes
apps/sim/hooks/queries/folders.ts useRestoreFolder hook added with correct onSettled invalidation of both folder scopes and workflow lists; new archivedAt field mapped in mapFolder
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsx Delete modal now shows 'You can restore it from Recently Deleted' for workflow/folder/mixed types; 'This action cannot be undone' correctly retained (muted) for workspace/task
apps/sim/hooks/queries/utils/folder-keys.ts Query key factory adds scope dimension correctly; lists() prefix enables targeted invalidation across all scopes
apps/sim/stores/folders/types.ts archivedAt field added but typed as '?: Date
packages/db/schema.ts archivedAt nullable column + btree index added to workflowFolder table; schema matches migration
packages/db/migrations/0186_greedy_jocasta.sql Additive migration: adds archived_at nullable column + btree index to workflow_folder; safe on existing data
apps/sim/lib/workflows/lifecycle.ts archiveWorkflowsByIdsInWorkspace correctly accepts shared archivedAt timestamp option used by folder deletion
apps/sim/lib/audit/log.ts FOLDER_RESTORED audit action added; FOLDER resource type was already present
apps/sim/lib/posthog/events.ts folder_restored event added to PostHog event catalog

Sequence Diagram

sequenceDiagram
    participant User
    participant API as folders API
    participant LC as folder-lifecycle.ts
    participant DB

    Note over User,DB: Soft-Delete (folder + contents)
    User->>API: DELETE /api/folders/{id}
    API->>LC: performDeleteFolder()
    LC->>DB: countWorkflowsInFolderRecursively()
    LC->>LC: deleteFolderRecursively(id, ws, timestamp T)
    loop Each non-archived child folder
        LC->>LC: deleteFolderRecursively(childId, ws, T)
    end
    LC->>DB: archiveWorkflowsByIdsInWorkspace(ids, archivedAt=T)
    LC->>DB: UPDATE workflowFolder SET archivedAt=T
    LC->>DB: recordAudit(FOLDER_DELETED)
    API->>User: 200 OK

    Note over User,DB: Restore flow
    User->>API: POST /api/folders/{id}/restore
    API->>LC: performRestoreFolder()
    LC->>DB: SELECT folder WHERE id AND workspaceId
    LC->>DB: BEGIN TRANSACTION
    alt parent folder is archived
        LC->>DB: UPDATE workflowFolder SET parentId=null
    end
    LC->>LC: restoreFolderRecursively(id, ws, folderArchivedAt=T, tx)
    LC->>DB: UPDATE workflowFolder SET archivedAt=null
    LC->>DB: SELECT workflows WHERE archivedAt=T (exact match)
    LC->>DB: UPDATE workflow+schedule+webhook+chat+form+mcp+a2a SET archivedAt=null
    loop Each child folder WHERE archivedAt=T
        LC->>LC: restoreFolderRecursively(childId, ws, T, tx)
    end
    LC->>DB: COMMIT
    LC->>DB: recordAudit(FOLDER_RESTORED)
    API->>User: 200 {success, restoredItems}
Loading

Reviews (5): Last reviewed commit: "fix(folders): handle missing parent fold..." | Re-trigger Greptile

…workflows

Address two review findings:
- Wrap entire folder restore in a single DB transaction to prevent
  partial state if any step fails
- Only restore workflows archived within 5s of the folder's archivedAt,
  so individually-deleted workflows are not silently un-deleted
- Add folder_restored to PostHog event map

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 5-second time window for scoping which workflows to restore was
a fragile heuristic (magic number, race-prone, non-deterministic).
Restoring a folder now restores all archived workflows in it, matching
standard trash/recycle-bin behavior. Users can re-delete any workflow
they don't want after restore.

The single-transaction wrapping from the prior commit is kept — that
was a legitimate atomicity fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
waleedlatif1 and others added 2 commits April 6, 2026 18:40
Replace manually created migration with proper drizzle-kit generated
one that includes the snapshot file, fixing CI schema sync check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

…mestamp

Use a single timestamp across the entire folder deletion — folders,
workflows, schedules, webhooks, etc. all get the exact same archivedAt.
On restore, match workflows by exact archivedAt equality with the
folder's timestamp, so individually-deleted workflows are not
silently un-deleted.

- Add optional archivedAt to ArchiveWorkflowOptions (backwards-compatible)
- Pass shared timestamp through deleteFolderRecursively → archiveWorkflowsByIdsInWorkspace
- Filter restore with eq(workflow.archivedAt, folderArchivedAt) instead of isNotNull

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…missing

When individually restoring a workflow from Recently Deleted, check if
its folder still exists and is active. If the folder is archived or
missing, clear folderId so the workflow appears at root instead of
being orphaned (invisible in sidebar).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Three issues caught by audit:

1. Child folder restore used isNotNull instead of timestamp matching,
   so individually-deleted child folders would be incorrectly restored.
   Now uses eq(archivedAt, folderArchivedAt) for both workflows AND
   child folders — consistent and deterministic.

2. No workspace archived check — could restore a folder into an
   archived workspace. Now checks getWorkspaceWithOwner, matching
   the existing restoreWorkflow pattern.

3. Re-restoring an already-restored folder returned an error. Now
   returns success with zero counts (idempotent).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ensures optimistic folder objects include archivedAt: null for
consistency with the database schema shape.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

If the parent folder row no longer exists (not just archived), the
restored folder now correctly gets reparented to root instead of
retaining a dangling parentId reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a80cc8b. Configure here.

@waleedlatif1 waleedlatif1 merged commit 89ae738 into staging Apr 7, 2026
12 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/sidebar-folders branch April 7, 2026 03:06
emir-karabeg pushed a commit that referenced this pull request Apr 7, 2026
* feat(folders): soft-delete folders and show in Recently Deleted

Folders are now soft-deleted (archived) instead of permanently removed,
matching the existing pattern for workflows, tables, and knowledge bases.
Users can restore folders from Settings > Recently Deleted.

- Add `archivedAt` column to `workflowFolder` schema with index
- Change folder deletion to set `archivedAt` instead of hard-delete
- Add folder restore endpoint (POST /api/folders/[id]/restore)
- Batch-restore all workflows inside restored folders in one transaction
- Add scope filter to GET /api/folders (active/archived)
- Add Folders tab to Recently Deleted settings page
- Update delete modal messaging for restorable items
- Change "This action cannot be undone" styling to muted text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(testing): add FOLDER_RESTORED to audit mock

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(folders): atomic restore transaction and scope to folder-deleted workflows

Address two review findings:
- Wrap entire folder restore in a single DB transaction to prevent
  partial state if any step fails
- Only restore workflows archived within 5s of the folder's archivedAt,
  so individually-deleted workflows are not silently un-deleted
- Add folder_restored to PostHog event map

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(folders): simplify restore to remove hacky 5s time window

The 5-second time window for scoping which workflows to restore was
a fragile heuristic (magic number, race-prone, non-deterministic).
Restoring a folder now restores all archived workflows in it, matching
standard trash/recycle-bin behavior. Users can re-delete any workflow
they don't want after restore.

The single-transaction wrapping from the prior commit is kept — that
was a legitimate atomicity fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(db): regenerate folder soft-delete migration with drizzle-kit

Replace manually created migration with proper drizzle-kit generated
one that includes the snapshot file, fixing CI schema sync check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(db): fix migration metadata formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(folders): scope restore to folder-deleted workflows via shared timestamp

Use a single timestamp across the entire folder deletion — folders,
workflows, schedules, webhooks, etc. all get the exact same archivedAt.
On restore, match workflows by exact archivedAt equality with the
folder's timestamp, so individually-deleted workflows are not
silently un-deleted.

- Add optional archivedAt to ArchiveWorkflowOptions (backwards-compatible)
- Pass shared timestamp through deleteFolderRecursively → archiveWorkflowsByIdsInWorkspace
- Filter restore with eq(workflow.archivedAt, folderArchivedAt) instead of isNotNull

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(workflows): clear folderId on restore when folder is archived or missing

When individually restoring a workflow from Recently Deleted, check if
its folder still exists and is active. If the folder is archived or
missing, clear folderId so the workflow appears at root instead of
being orphaned (invisible in sidebar).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(folders): format restoreFolderRecursively call to satisfy biome

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(folders): close remaining restore edge cases

Three issues caught by audit:

1. Child folder restore used isNotNull instead of timestamp matching,
   so individually-deleted child folders would be incorrectly restored.
   Now uses eq(archivedAt, folderArchivedAt) for both workflows AND
   child folders — consistent and deterministic.

2. No workspace archived check — could restore a folder into an
   archived workspace. Now checks getWorkspaceWithOwner, matching
   the existing restoreWorkflow pattern.

3. Re-restoring an already-restored folder returned an error. Now
   returns success with zero counts (idempotent).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(folders): add archivedAt to optimistic folder creation objects

Ensures optimistic folder objects include archivedAt: null for
consistency with the database schema shape.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(folders): handle missing parent folder during restore reparenting

If the parent folder row no longer exists (not just archived), the
restored folder now correctly gets reparented to root instead of
retaining a dangling parentId reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant