Skip to content

Add restore endpoints and ui#3570

Merged
TheodoreSpeaks merged 6 commits intofeat/mothership-copilotfrom
feat/restore
Mar 14, 2026
Merged

Add restore endpoints and ui#3570
TheodoreSpeaks merged 6 commits intofeat/mothership-copilotfrom
feat/restore

Conversation

@TheodoreSpeaks
Copy link
Collaborator

Summary

WIP
Fixes #(issue)

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

How has this been tested? What should reviewers focus on?

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Screenshots/Videos

@cursor
Copy link

cursor bot commented Mar 14, 2026

PR Summary

Medium Risk
Adds new restore endpoints and database mutations for workflows/tables/knowledge bases/files, which can impact data integrity and permissions if mis-scoped. Also refactors workflow notifications to use a new global toast provider, changing user-visible messaging behavior.

Overview
Adds a Recently Deleted settings section that lists archived workflows, tables, knowledge bases, and files (with search + type tabs) and allows restoring them, showing a toast with a View action on success.

Introduces new POST .../restore API routes for each resource with permission checks, and implements corresponding restore operations in services/hooks (including restoring workflow-related child records and KB/table/file archived/deleted flags while blocking restores into archived workspaces).

Replaces the custom in-workflow notification UI/animations with a shared ToastProvider/toast() system and updates delete confirmation copy across the app to reflect that items can be restored.

Written by Cursor Bugbot for commit aab1381. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Mar 14, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 14, 2026 1:49am

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR adds a "Recently Deleted" feature, allowing users to restore soft-deleted workflows, tables, knowledge bases, and workspace files from a new Settings section. It introduces four new POST /restore API endpoints, corresponding service-layer restore functions, React Query mutation hooks, a RecentlyDeleted settings page component, and a custom ToastProvider for in-app notifications.

Key changes and issues found:

  • Security gap in workflow restore endpoint (apps/sim/app/api/workflows/[id]/restore/route.ts): When a workflow has no workspaceId (personal workflow), there is no ownership check — any authenticated user who knows the workflow ID can restore it. The knowledge base endpoint correctly handles this with an else if (kb.userId !== auth.userId) guard, but the workflow endpoint is missing the equivalent.
  • Inaccurate "Deleted" timestamps in the UI: Workflows display lastModified (mapped from updatedAt) and knowledge bases display updatedAt as their deletion date — neither is the actual archivedAt / deletedAt timestamp, so the shown date can be misleading.
  • Restore errors are silently swallowed: handleRestore has onSuccess and onSettled callbacks but no onError — if a restore call fails, the spinner disappears but the user receives no feedback.
  • AUTO_DISMISS_MS = 0 in toast.tsx: All toasts persist indefinitely until manually dismissed, which will accumulate and clutter the UI for transient success messages.
  • globals.css modified for toast keyframes (rule violation): The @keyframes toast-enter / @keyframes toast-exit animations should be scoped to the toast component, not added to the global stylesheet.
  • restorableTypes Set allocated on every render in delete-modal.tsx — a minor inefficiency that is easily resolved by moving it outside the component.

Confidence Score: 2/5

  • Not safe to merge — the workflow restore endpoint is missing an ownership check for personal workflows, and inaccurate deletion timestamps in the UI are misleading.
  • The service-layer restore logic is solid (transactions, workspace-archived guards, correct child-entity handling), but the workflow API route has a missing authorization branch that could allow any authenticated user to restore another user's personal workflow. Additionally, the "Deleted" date shown in the UI for workflows and knowledge bases is unreliable, and restore failures give the user no error feedback. These issues should be resolved before merging.
  • Pay close attention to apps/sim/app/api/workflows/[id]/restore/route.ts (missing ownership check) and apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx (incorrect deletion timestamps, missing error handling).

Important Files Changed

Filename Overview
apps/sim/app/api/workflows/[id]/restore/route.ts New restore endpoint for workflows — missing ownership check for personal (non-workspace) workflows, allowing any authenticated user to restore another user's workflow if they know the ID.
apps/sim/app/api/knowledge/[id]/restore/route.ts New restore endpoint for knowledge bases — correctly checks both workspace permissions and personal ownership via kb.userId !== auth.userId.
apps/sim/components/emcn/components/toast/toast.tsx New custom toast implementation using React context and a module-level global singleton; AUTO_DISMISS_MS = 0 means toasts never auto-dismiss, which is a UX concern.
apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx New "Recently Deleted" settings section — workflow deletedAt uses lastModified (i.e. updatedAt) and KB deletedAt uses updatedAt rather than actual deletion timestamps; restore errors are silently swallowed with no user feedback.
apps/sim/app/_styles/globals.css Added @keyframes toast-enter and @keyframes toast-exit — these should live in the toast component file, not in globals.css, per project style conventions.
apps/sim/lib/workflows/lifecycle.ts Adds restoreWorkflow — correctly restores the workflow and all related entities (schedules, webhooks, chats, forms, MCP tools, A2A agents) within a single transaction.
apps/sim/lib/knowledge/service.ts Adds restoreKnowledgeBase — correctly uses a SELECT … FOR UPDATE advisory lock and restores documents/connectors that were archived as part of the KB snapshot without touching directly-deleted children.
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsx Updated delete confirmation text and added restorable-type guard — restorableTypes Set is needlessly recreated on every render.

Sequence Diagram

sequenceDiagram
    participant U as User (Recently Deleted UI)
    participant RD as RecentlyDeleted Component
    participant Q as React Query Hooks
    participant API as REST API Routes
    participant SVC as Service Layer (lib/)
    participant DB as Database

    U->>RD: Visit Settings → Recently Deleted
    RD->>Q: useWorkflows(scope: archived)
    RD->>Q: useTablesList(archived)
    RD->>Q: useKnowledgeBasesQuery(scope: archived)
    RD->>Q: useWorkspaceFiles(archived)
    Q->>API: GET /api/workflows?scope=archived
    Q->>API: GET /api/tables?scope=archived
    Q->>API: GET /api/knowledge?scope=archived
    Q->>API: GET /api/workspaces/{id}/files?scope=archived
    API-->>Q: Archived resource lists
    Q-->>RD: Merged & sorted DeletedResource[]

    U->>RD: Click "Restore" on an item
    RD->>Q: mutate(resourceId) via useRestoreWorkflow / useRestoreTable / useRestoreKnowledgeBase / useRestoreWorkspaceFile
    Q->>API: POST /api/workflows/{id}/restore
    Note over API: Auth check + permission check
    API->>SVC: restoreWorkflow(id, requestId)
    SVC->>DB: BEGIN TRANSACTION
    SVC->>DB: UPDATE workflow SET archivedAt = NULL
    SVC->>DB: UPDATE schedules, webhooks, chats, forms, mcp_tools, a2a_agents SET archivedAt = NULL
    DB-->>SVC: OK
    SVC-->>API: { restored: true }
    API-->>Q: 200 { success: true }
    Q->>Q: invalidateQueries(workflowKeys.lists())
    Q-->>RD: onSuccess → toast.success("Workflow restored")
    RD-->>U: Toast shown + item removed from list
Loading

Comments Outside Diff (6)

  1. apps/sim/components/emcn/components/toast/toast.tsx, line 879 (link)

    Toasts never auto-dismiss with AUTO_DISMISS_MS = 0

    AUTO_DISMISS_MS is set to 0, and the dismiss timer only fires when t.duration > 0:

    useEffect(() => {
      if (t.duration > 0) {
        timerRef.current = setTimeout(dismiss, t.duration)
        return () => clearTimeout(timerRef.current)
      }
    }, [dismiss, t.duration])

    This means every toast — including success confirmations after restoring an item — will remain on screen indefinitely until the user manually clicks ✕. Up to 20 toasts can accumulate simultaneously (MAX_VISIBLE = 20). A sensible default (e.g. 4000–5000 ms) would provide better UX for transient feedback toasts.

  2. apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx, line 539-542 (link)

    Incorrect "Deleted" timestamp shown for workflows

    wf.lastModified maps to workflow.updatedAt (see mapWorkflow in hooks/queries/workflows.ts), not to archivedAt. Workflows that were frequently edited before deletion will show their last-edit date instead of their actual deletion date, making the "Deleted on …" label misleading.

    The useWorkflows hook (via the API route) fetches raw workflow rows which include archivedAt. Consider exposing archivedAt in WorkflowMetadata and the mapWorkflow function so it can be used here, or derive the timestamp server-side and return it in the archived-scope response.

  3. apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx, line 555-563 (link)

    Knowledge base deletedAt uses updatedAt instead of deletedAt

    kb.updatedAt is set to now both when a knowledge base is deleted and when it is restored (see restoreKnowledgeBase in lib/knowledge/service.ts). Using updatedAt as the deletion timestamp is therefore unreliable. The knowledgeBase schema has a dedicated deletedAt column — it should be returned by useKnowledgeBasesQuery (scoped to 'archived') and used here instead.

  4. apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx, line 590-624 (link)

    No onError handler — restore failures are silently swallowed

    handleRestore calls .mutate(…, { onSettled, onSuccess }) but provides no onError callback. If the server returns an error (e.g. network failure, permission denied), the spinner disappears (via onSettled) but the user receives no feedback about what went wrong. A minimal error toast would close the loop:

    const onError = (err: unknown) => {
      toast.error(err instanceof Error ? err.message : `Failed to restore ${resource.name}`)
    }

    and pass onError alongside onSettled and onSuccess in each .mutate(…) call.

  5. apps/sim/app/api/workflows/[id]/restore/route.ts, line 183-192 (link)

    Missing ownership check for personal (non-workspace) workflows

    When workflowData.workspaceId is falsy the code falls through without any authorization check, meaning any authenticated user who discovers a workflow ID can restore it. The knowledge base restore endpoint handles this symmetrically with:

    } else if (kb.userId !== auth.userId) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    The workflow route should include the same guard:

        if (workflowData.workspaceId) {
          const permission = await getUserEntityPermissions(
            auth.userId,
            'workspace',
            workflowData.workspaceId
          )
          if (permission !== 'admin' && permission !== 'write') {
            return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
          }
        } else if (workflowData.userId !== auth.userId) {
          return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
        }
  6. apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsx, line 802 (link)

    restorableTypes recreated on every render

    restorableTypes is a new Set<string>(['workflow']) created inside the component body, so a fresh Set is allocated on every render. Since the value is static, it should be moved outside the component as a module-level constant:

    and reference RESTORABLE_TYPES.has(itemType) in the JSX below.

Last reviewed commit: 87c36c7


type ResourceType = 'all' | 'workflow' | 'table' | 'knowledge' | 'file'

const ICON_CLASS = 'h-[14px] w-[14px]'
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

is this correct or can this also be defined?

.update(a2aAgent)
.set({ archivedAt: null, updatedAt: now })
.where(eq(a2aAgent.workflowId, workflowId))
})
Copy link

Choose a reason for hiding this comment

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

Workflow restore leaves triggers permanently disabled after restore

High Severity

archiveWorkflow explicitly disables operational state on children — setting status: 'disabled' on schedules, isActive: false on webhooks/chats/forms, isPublished: false on A2A agents, and isDeployed: false/isPublicApi: false on the workflow itself. But restoreWorkflow only clears archivedAt without restoring any of these flags. It also skips workflowDeploymentVersion entirely. After a restore, the workflow and all its triggers remain effectively dead. The knowledge base restore correctly sets connector status: 'active', showing the expected pattern.

Fix in Cursor Fix in Web

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Making the decision now that workflows should restore back to undeployed. Users can manually redeploy after the fact.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Certain booleans like chat and agent we cannot currently infer whether they were deployed or not so hard to go back.

Copy link

@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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@TheodoreSpeaks TheodoreSpeaks merged commit f5ae468 into feat/mothership-copilot Mar 14, 2026
4 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the feat/restore branch March 14, 2026 02:03
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.

3 participants