From 92403e0594baf172c6504b935afcb9ecb8e084eb Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 30 Jan 2026 15:22:46 -0800 Subject: [PATCH 1/7] fix(editor): advanced toggle respects user edit permissions (#3089) --- .../components/panel/components/editor/editor.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index a951af1fa..3d47e1fa4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -150,7 +150,9 @@ export function Editor() { blockSubBlockValues, canonicalIndex ) - const displayAdvancedOptions = advancedMode || advancedValuesPresent + const displayAdvancedOptions = userPermissions.canEdit + ? advancedMode + : advancedMode || advancedValuesPresent const hasAdvancedOnlyFields = useMemo(() => { for (const subBlock of subBlocksForCanonical) { From 2d799b3272bab7d39c0bc7082e962127c0f4a74b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 30 Jan 2026 17:01:16 -0800 Subject: [PATCH 2/7] fix(billing): plan should be detected from stripe subscription object (#3090) * fix(billing): plan should be detected from stripe subscription object * fix typing --- apps/sim/lib/auth/auth.ts | 75 ++++++++++++++++++++++++++++++----- apps/sim/lib/billing/plans.ts | 31 +++++++++++++++ 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 707f18be6..9241eaf09 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -30,7 +30,7 @@ import { ensureOrganizationForTeamSubscription, syncSubscriptionUsageLimits, } from '@/lib/billing/organization' -import { getPlans } from '@/lib/billing/plans' +import { getPlans, resolvePlanFromStripeSubscription } from '@/lib/billing/plans' import { syncSeatsFromStripeQuantity } from '@/lib/billing/validation/seat-management' import { handleChargeDispute, handleDisputeClosed } from '@/lib/billing/webhooks/disputes' import { handleManualEnterpriseSubscription } from '@/lib/billing/webhooks/enterprise' @@ -2641,29 +2641,42 @@ export const auth = betterAuth({ } }, onSubscriptionComplete: async ({ + stripeSubscription, subscription, }: { event: Stripe.Event stripeSubscription: Stripe.Subscription subscription: any }) => { + const { priceId, planFromStripe, isTeamPlan } = + resolvePlanFromStripeSubscription(stripeSubscription) + logger.info('[onSubscriptionComplete] Subscription created', { subscriptionId: subscription.id, referenceId: subscription.referenceId, - plan: subscription.plan, + dbPlan: subscription.plan, + planFromStripe, + priceId, status: subscription.status, }) + const subscriptionForOrgCreation = isTeamPlan + ? { ...subscription, plan: 'team' } + : subscription + let resolvedSubscription = subscription try { - resolvedSubscription = await ensureOrganizationForTeamSubscription(subscription) + resolvedSubscription = await ensureOrganizationForTeamSubscription( + subscriptionForOrgCreation + ) } catch (orgError) { logger.error( '[onSubscriptionComplete] Failed to ensure organization for team subscription', { subscriptionId: subscription.id, referenceId: subscription.referenceId, - plan: subscription.plan, + dbPlan: subscription.plan, + planFromStripe, error: orgError instanceof Error ? orgError.message : String(orgError), stack: orgError instanceof Error ? orgError.stack : undefined, } @@ -2684,22 +2697,67 @@ export const auth = betterAuth({ event: Stripe.Event subscription: any }) => { + const stripeSubscription = event.data.object as Stripe.Subscription + const { priceId, planFromStripe, isTeamPlan } = + resolvePlanFromStripeSubscription(stripeSubscription) + + if (priceId && !planFromStripe) { + logger.warn( + '[onSubscriptionUpdate] Could not determine plan from Stripe price ID', + { + subscriptionId: subscription.id, + priceId, + dbPlan: subscription.plan, + } + ) + } + + const isUpgradeToTeam = + isTeamPlan && + subscription.plan !== 'team' && + !subscription.referenceId.startsWith('org_') + + const effectivePlanForTeamFeatures = planFromStripe ?? subscription.plan + logger.info('[onSubscriptionUpdate] Subscription updated', { subscriptionId: subscription.id, status: subscription.status, - plan: subscription.plan, + dbPlan: subscription.plan, + planFromStripe, + isUpgradeToTeam, + referenceId: subscription.referenceId, }) + const subscriptionForOrgCreation = isUpgradeToTeam + ? { ...subscription, plan: 'team' } + : subscription + let resolvedSubscription = subscription try { - resolvedSubscription = await ensureOrganizationForTeamSubscription(subscription) + resolvedSubscription = await ensureOrganizationForTeamSubscription( + subscriptionForOrgCreation + ) + + if (isUpgradeToTeam) { + logger.info( + '[onSubscriptionUpdate] Detected Pro -> Team upgrade, ensured organization creation', + { + subscriptionId: subscription.id, + originalPlan: subscription.plan, + newPlan: planFromStripe, + resolvedReferenceId: resolvedSubscription.referenceId, + } + ) + } } catch (orgError) { logger.error( '[onSubscriptionUpdate] Failed to ensure organization for team subscription', { subscriptionId: subscription.id, referenceId: subscription.referenceId, - plan: subscription.plan, + dbPlan: subscription.plan, + planFromStripe, + isUpgradeToTeam, error: orgError instanceof Error ? orgError.message : String(orgError), stack: orgError instanceof Error ? orgError.stack : undefined, } @@ -2717,9 +2775,8 @@ export const auth = betterAuth({ }) } - if (resolvedSubscription.plan === 'team') { + if (effectivePlanForTeamFeatures === 'team') { try { - const stripeSubscription = event.data.object as Stripe.Subscription const quantity = stripeSubscription.items?.data?.[0]?.quantity || 1 const result = await syncSeatsFromStripeQuantity( diff --git a/apps/sim/lib/billing/plans.ts b/apps/sim/lib/billing/plans.ts index d8f88ec49..4a767e41c 100644 --- a/apps/sim/lib/billing/plans.ts +++ b/apps/sim/lib/billing/plans.ts @@ -1,3 +1,4 @@ +import type Stripe from 'stripe' import { getFreeTierLimit, getProTierLimit, @@ -56,6 +57,13 @@ export function getPlanByName(planName: string): BillingPlan | undefined { return getPlans().find((plan) => plan.name === planName) } +/** + * Get a specific plan by Stripe price ID + */ +export function getPlanByPriceId(priceId: string): BillingPlan | undefined { + return getPlans().find((plan) => plan.priceId === priceId) +} + /** * Get plan limits for a given plan name */ @@ -63,3 +71,26 @@ export function getPlanLimits(planName: string): number { const plan = getPlanByName(planName) return plan?.limits.cost ?? getFreeTierLimit() } + +export interface StripePlanResolution { + priceId: string | undefined + planFromStripe: string | null + isTeamPlan: boolean +} + +/** + * Resolve plan information from a Stripe subscription object. + * Used to get the authoritative plan from Stripe rather than relying on DB state. + */ +export function resolvePlanFromStripeSubscription( + stripeSubscription: Stripe.Subscription +): StripePlanResolution { + const priceId = stripeSubscription?.items?.data?.[0]?.price?.id + const plan = priceId ? getPlanByPriceId(priceId) : undefined + + return { + priceId, + planFromStripe: plan?.name ?? null, + isTeamPlan: plan?.name === 'team', + } +} From 37d5e01f5fe35868cd32082a27bff9f1eb365dc9 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 30 Jan 2026 17:51:05 -0800 Subject: [PATCH 3/7] fix(mcp): increase timeout from 1m to 10m (#3093) --- apps/sim/app/api/mcp/serve/[serverId]/route.ts | 2 +- apps/sim/lib/mcp/shared.ts | 2 +- apps/sim/lib/mcp/utils.test.ts | 8 ++++---- apps/sim/lib/mcp/utils.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.ts index baa33e205..1bc217177 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.ts @@ -264,7 +264,7 @@ async function handleToolsCall( method: 'POST', headers, body: JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp' }), - signal: AbortSignal.timeout(300000), // 5 minute timeout + signal: AbortSignal.timeout(600000), // 10 minute timeout }) const executeResult = await response.json() diff --git a/apps/sim/lib/mcp/shared.ts b/apps/sim/lib/mcp/shared.ts index e865aae6e..3f3bdae66 100644 --- a/apps/sim/lib/mcp/shared.ts +++ b/apps/sim/lib/mcp/shared.ts @@ -34,7 +34,7 @@ export function sanitizeHeaders( * Client-safe MCP constants */ export const MCP_CLIENT_CONSTANTS = { - CLIENT_TIMEOUT: 60000, + CLIENT_TIMEOUT: 600000, MAX_RETRIES: 3, RECONNECT_DELAY: 1000, } as const diff --git a/apps/sim/lib/mcp/utils.test.ts b/apps/sim/lib/mcp/utils.test.ts index 518e0bb23..1b0cabf98 100644 --- a/apps/sim/lib/mcp/utils.test.ts +++ b/apps/sim/lib/mcp/utils.test.ts @@ -81,8 +81,8 @@ describe('generateMcpServerId', () => { }) describe('MCP_CONSTANTS', () => { - it.concurrent('has correct execution timeout', () => { - expect(MCP_CONSTANTS.EXECUTION_TIMEOUT).toBe(60000) + it.concurrent('has correct execution timeout (10 minutes)', () => { + expect(MCP_CONSTANTS.EXECUTION_TIMEOUT).toBe(600000) }) it.concurrent('has correct cache timeout (5 minutes)', () => { @@ -107,8 +107,8 @@ describe('MCP_CONSTANTS', () => { }) describe('MCP_CLIENT_CONSTANTS', () => { - it.concurrent('has correct client timeout', () => { - expect(MCP_CLIENT_CONSTANTS.CLIENT_TIMEOUT).toBe(60000) + it.concurrent('has correct client timeout (10 minutes)', () => { + expect(MCP_CLIENT_CONSTANTS.CLIENT_TIMEOUT).toBe(600000) }) it.concurrent('has correct auto refresh interval (5 minutes)', () => { diff --git a/apps/sim/lib/mcp/utils.ts b/apps/sim/lib/mcp/utils.ts index a732dc382..ab28a66ee 100644 --- a/apps/sim/lib/mcp/utils.ts +++ b/apps/sim/lib/mcp/utils.ts @@ -6,7 +6,7 @@ import { isMcpTool, MCP } from '@/executor/constants' * MCP-specific constants */ export const MCP_CONSTANTS = { - EXECUTION_TIMEOUT: 60000, + EXECUTION_TIMEOUT: 600000, CACHE_TIMEOUT: 5 * 60 * 1000, DEFAULT_RETRIES: 3, DEFAULT_CONNECTION_TIMEOUT: 30000, @@ -49,7 +49,7 @@ export function sanitizeHeaders( * Client-safe MCP constants */ export const MCP_CLIENT_CONSTANTS = { - CLIENT_TIMEOUT: 60000, + CLIENT_TIMEOUT: 600000, AUTO_REFRESH_INTERVAL: 5 * 60 * 1000, } as const From 4109feecf6d1405c0030554949d80202ac017d98 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 30 Jan 2026 18:39:23 -0800 Subject: [PATCH 4/7] feat(invitations): added invitations query hook, migrated all tool files to use absolute imports (#3092) * feat(invitations): added invitations query hook, migrated all tool files to use absolute imports * ack PR comments * remove dead import * remove unused hook --- .../notifications/notifications.tsx | 8 +- .../workspace-permissions-provider.tsx | 88 ++- .../settings-modal/components/files/files.tsx | 7 +- .../components/permissions-table-skeleton.tsx | 23 - .../components/permissions-table.tsx | 28 +- .../components/invite-modal/index.ts | 1 - .../components/invite-modal/invite-modal.tsx | 582 +++++++----------- .../w/components/sidebar/hooks/index.ts | 2 +- .../sidebar/hooks/use-workspace-management.ts | 363 ++++------- apps/sim/hooks/queries/invitations.ts | 309 ++++++++++ apps/sim/hooks/queries/oauth-connections.ts | 92 ++- apps/sim/hooks/queries/workspace.ts | 208 ++++++- apps/sim/hooks/use-slack-accounts.ts | 52 -- apps/sim/hooks/use-user-permissions.ts | 4 +- apps/sim/hooks/use-workspace-permissions.ts | 107 ---- apps/sim/tools/a2a/cancel_task.ts | 4 +- .../sim/tools/a2a/delete_push_notification.ts | 7 +- apps/sim/tools/a2a/get_agent_card.ts | 4 +- apps/sim/tools/a2a/get_push_notification.ts | 7 +- apps/sim/tools/a2a/get_task.ts | 4 +- apps/sim/tools/a2a/resubscribe.ts | 4 +- apps/sim/tools/a2a/send_message.ts | 4 +- apps/sim/tools/a2a/set_push_notification.ts | 7 +- apps/sim/tools/apify/run_actor_async.ts | 2 +- apps/sim/tools/apify/run_actor_sync.ts | 2 +- apps/sim/tools/google_groups/add_alias.ts | 5 +- apps/sim/tools/google_groups/add_member.ts | 2 +- apps/sim/tools/google_groups/create_group.ts | 2 +- apps/sim/tools/google_groups/delete_group.ts | 2 +- apps/sim/tools/google_groups/get_group.ts | 2 +- apps/sim/tools/google_groups/get_member.ts | 2 +- apps/sim/tools/google_groups/get_settings.ts | 5 +- apps/sim/tools/google_groups/has_member.ts | 2 +- apps/sim/tools/google_groups/list_aliases.ts | 5 +- apps/sim/tools/google_groups/list_groups.ts | 2 +- apps/sim/tools/google_groups/list_members.ts | 5 +- apps/sim/tools/google_groups/remove_alias.ts | 5 +- apps/sim/tools/google_groups/remove_member.ts | 5 +- apps/sim/tools/google_groups/update_group.ts | 2 +- apps/sim/tools/google_groups/update_member.ts | 5 +- .../tools/google_groups/update_settings.ts | 5 +- apps/sim/tools/http/webhook_request.ts | 2 +- .../incidentio/incident_statuses_list.ts | 4 +- .../tools/incidentio/incident_types_list.ts | 4 +- apps/sim/tools/incidentio/severities_list.ts | 5 +- apps/sim/tools/incidentio/users_list.ts | 5 +- apps/sim/tools/incidentio/users_show.ts | 5 +- apps/sim/tools/incidentio/workflows_create.ts | 2 +- apps/sim/tools/incidentio/workflows_delete.ts | 2 +- apps/sim/tools/incidentio/workflows_list.ts | 2 +- apps/sim/tools/incidentio/workflows_show.ts | 2 +- apps/sim/tools/incidentio/workflows_update.ts | 2 +- apps/sim/tools/intercom/get_company.ts | 3 - apps/sim/tools/intercom/get_conversation.ts | 3 - apps/sim/tools/intercom/list_companies.ts | 3 - apps/sim/tools/intercom/list_contacts.ts | 3 - apps/sim/tools/intercom/list_conversations.ts | 3 - apps/sim/tools/intercom/reply_conversation.ts | 3 - apps/sim/tools/intercom/search_contacts.ts | 3 - apps/sim/tools/kalshi/amend_order.ts | 4 +- apps/sim/tools/kalshi/cancel_order.ts | 4 +- apps/sim/tools/kalshi/create_order.ts | 4 +- apps/sim/tools/kalshi/get_balance.ts | 4 +- apps/sim/tools/kalshi/get_candlesticks.ts | 4 +- apps/sim/tools/kalshi/get_event.ts | 4 +- apps/sim/tools/kalshi/get_events.ts | 8 +- apps/sim/tools/kalshi/get_exchange_status.ts | 4 +- apps/sim/tools/kalshi/get_fills.ts | 6 +- apps/sim/tools/kalshi/get_market.ts | 4 +- apps/sim/tools/kalshi/get_markets.ts | 6 +- apps/sim/tools/kalshi/get_order.ts | 4 +- apps/sim/tools/kalshi/get_orderbook.ts | 4 +- apps/sim/tools/kalshi/get_orders.ts | 6 +- apps/sim/tools/kalshi/get_positions.ts | 6 +- apps/sim/tools/kalshi/get_series_by_ticker.ts | 4 +- apps/sim/tools/kalshi/get_trades.ts | 8 +- apps/sim/tools/kalshi/types.ts | 5 - apps/sim/tools/mailchimp/add_member.ts | 6 +- apps/sim/tools/mailchimp/add_member_tags.ts | 2 +- .../tools/mailchimp/add_or_update_member.ts | 6 +- .../sim/tools/mailchimp/add_segment_member.ts | 9 +- .../mailchimp/add_subscriber_to_automation.ts | 7 +- apps/sim/tools/mailchimp/archive_member.ts | 5 +- apps/sim/tools/mailchimp/create_audience.ts | 4 +- .../tools/mailchimp/create_batch_operation.ts | 4 +- apps/sim/tools/mailchimp/create_campaign.ts | 6 +- apps/sim/tools/mailchimp/create_interest.ts | 7 +- .../mailchimp/create_interest_category.ts | 7 +- .../tools/mailchimp/create_landing_page.ts | 7 +- .../sim/tools/mailchimp/create_merge_field.ts | 7 +- apps/sim/tools/mailchimp/create_segment.ts | 4 +- apps/sim/tools/mailchimp/create_template.ts | 5 +- apps/sim/tools/mailchimp/delete_audience.ts | 5 +- .../tools/mailchimp/delete_batch_operation.ts | 5 +- apps/sim/tools/mailchimp/delete_campaign.ts | 5 +- apps/sim/tools/mailchimp/delete_interest.ts | 5 +- .../mailchimp/delete_interest_category.ts | 5 +- .../tools/mailchimp/delete_landing_page.ts | 5 +- apps/sim/tools/mailchimp/delete_member.ts | 5 +- .../sim/tools/mailchimp/delete_merge_field.ts | 5 +- apps/sim/tools/mailchimp/delete_segment.ts | 5 +- apps/sim/tools/mailchimp/delete_template.ts | 5 +- apps/sim/tools/mailchimp/get_audience.ts | 7 +- apps/sim/tools/mailchimp/get_audiences.ts | 7 +- apps/sim/tools/mailchimp/get_automation.ts | 7 +- apps/sim/tools/mailchimp/get_automations.ts | 7 +- .../tools/mailchimp/get_batch_operation.ts | 7 +- .../tools/mailchimp/get_batch_operations.ts | 7 +- apps/sim/tools/mailchimp/get_campaign.ts | 9 +- .../tools/mailchimp/get_campaign_content.ts | 9 +- .../tools/mailchimp/get_campaign_report.ts | 9 +- .../tools/mailchimp/get_campaign_reports.ts | 9 +- apps/sim/tools/mailchimp/get_campaigns.ts | 9 +- apps/sim/tools/mailchimp/get_interest.ts | 7 +- .../mailchimp/get_interest_categories.ts | 5 +- .../tools/mailchimp/get_interest_category.ts | 7 +- apps/sim/tools/mailchimp/get_interests.ts | 5 +- apps/sim/tools/mailchimp/get_landing_page.ts | 7 +- apps/sim/tools/mailchimp/get_landing_pages.ts | 7 +- apps/sim/tools/mailchimp/get_member.ts | 9 +- apps/sim/tools/mailchimp/get_member_tags.ts | 5 +- apps/sim/tools/mailchimp/get_members.ts | 9 +- apps/sim/tools/mailchimp/get_merge_field.ts | 5 +- apps/sim/tools/mailchimp/get_merge_fields.ts | 7 +- apps/sim/tools/mailchimp/get_segment.ts | 7 +- .../tools/mailchimp/get_segment_members.ts | 7 +- apps/sim/tools/mailchimp/get_segments.ts | 5 +- apps/sim/tools/mailchimp/get_template.ts | 7 +- apps/sim/tools/mailchimp/get_templates.ts | 7 +- apps/sim/tools/mailchimp/pause_automation.ts | 2 +- .../tools/mailchimp/publish_landing_page.ts | 2 +- .../sim/tools/mailchimp/remove_member_tags.ts | 2 +- .../tools/mailchimp/remove_segment_member.ts | 2 +- .../sim/tools/mailchimp/replicate_campaign.ts | 2 +- apps/sim/tools/mailchimp/schedule_campaign.ts | 2 +- apps/sim/tools/mailchimp/send_campaign.ts | 2 +- .../tools/mailchimp/set_campaign_content.ts | 6 +- apps/sim/tools/mailchimp/start_automation.ts | 2 +- apps/sim/tools/mailchimp/unarchive_member.ts | 6 +- .../tools/mailchimp/unpublish_landing_page.ts | 2 +- .../tools/mailchimp/unschedule_campaign.ts | 2 +- apps/sim/tools/mailchimp/update_audience.ts | 4 +- apps/sim/tools/mailchimp/update_campaign.ts | 6 +- apps/sim/tools/mailchimp/update_interest.ts | 4 +- .../mailchimp/update_interest_category.ts | 4 +- .../tools/mailchimp/update_landing_page.ts | 4 +- apps/sim/tools/mailchimp/update_member.ts | 6 +- .../sim/tools/mailchimp/update_merge_field.ts | 4 +- apps/sim/tools/mailchimp/update_segment.ts | 4 +- apps/sim/tools/mailchimp/update_template.ts | 4 +- apps/sim/tools/search/tool.ts | 2 +- apps/sim/tools/shopify/adjust_inventory.ts | 4 +- apps/sim/tools/shopify/cancel_order.ts | 4 +- apps/sim/tools/shopify/create_customer.ts | 4 +- apps/sim/tools/shopify/create_fulfillment.ts | 4 +- apps/sim/tools/shopify/create_product.ts | 4 +- apps/sim/tools/shopify/delete_customer.ts | 2 +- apps/sim/tools/shopify/delete_product.ts | 2 +- apps/sim/tools/shopify/get_collection.ts | 4 +- apps/sim/tools/shopify/get_customer.ts | 4 +- apps/sim/tools/shopify/get_inventory_level.ts | 7 +- apps/sim/tools/shopify/get_order.ts | 4 +- apps/sim/tools/shopify/get_product.ts | 4 +- apps/sim/tools/shopify/list_collections.ts | 4 +- apps/sim/tools/shopify/list_customers.ts | 4 +- .../sim/tools/shopify/list_inventory_items.ts | 7 +- apps/sim/tools/shopify/list_locations.ts | 4 +- apps/sim/tools/shopify/list_orders.ts | 4 +- apps/sim/tools/shopify/list_products.ts | 4 +- apps/sim/tools/shopify/update_customer.ts | 4 +- apps/sim/tools/shopify/update_order.ts | 4 +- apps/sim/tools/shopify/update_product.ts | 4 +- apps/sim/tools/similarweb/bounce_rate.ts | 5 +- apps/sim/tools/similarweb/pages_per_visit.ts | 5 +- apps/sim/tools/similarweb/traffic_visits.ts | 5 +- apps/sim/tools/similarweb/visit_duration.ts | 5 +- apps/sim/tools/similarweb/website_overview.ts | 5 +- apps/sim/tools/spotify/add_to_queue.ts | 2 +- .../tools/spotify/add_tracks_to_playlist.ts | 5 +- apps/sim/tools/spotify/check_saved_tracks.ts | 5 +- apps/sim/tools/spotify/create_playlist.ts | 5 +- apps/sim/tools/spotify/get_album.ts | 6 +- apps/sim/tools/spotify/get_album_tracks.ts | 2 +- apps/sim/tools/spotify/get_albums.ts | 2 +- apps/sim/tools/spotify/get_artist.ts | 2 +- apps/sim/tools/spotify/get_artist_albums.ts | 5 +- .../tools/spotify/get_artist_top_tracks.ts | 7 +- apps/sim/tools/spotify/get_artists.ts | 2 +- apps/sim/tools/spotify/get_categories.ts | 5 +- apps/sim/tools/spotify/get_current_user.ts | 5 +- .../tools/spotify/get_currently_playing.ts | 2 +- apps/sim/tools/spotify/get_devices.ts | 2 +- .../sim/tools/spotify/get_followed_artists.ts | 2 +- apps/sim/tools/spotify/get_new_releases.ts | 7 +- apps/sim/tools/spotify/get_playback_state.ts | 10 +- apps/sim/tools/spotify/get_playlist.ts | 4 +- apps/sim/tools/spotify/get_playlist_tracks.ts | 7 +- apps/sim/tools/spotify/get_queue.ts | 2 +- apps/sim/tools/spotify/get_recently_played.ts | 7 +- apps/sim/tools/spotify/get_saved_albums.ts | 2 +- apps/sim/tools/spotify/get_saved_tracks.ts | 7 +- apps/sim/tools/spotify/get_top_artists.ts | 2 +- apps/sim/tools/spotify/get_top_tracks.ts | 7 +- apps/sim/tools/spotify/get_track.ts | 7 +- apps/sim/tools/spotify/get_tracks.ts | 7 +- apps/sim/tools/spotify/get_user_playlists.ts | 5 +- apps/sim/tools/spotify/pause.ts | 2 +- apps/sim/tools/spotify/play.ts | 2 +- .../spotify/remove_tracks_from_playlist.ts | 4 +- apps/sim/tools/spotify/save_tracks.ts | 2 +- apps/sim/tools/spotify/search.ts | 6 +- apps/sim/tools/spotify/set_volume.ts | 2 +- apps/sim/tools/spotify/skip_next.ts | 2 +- apps/sim/tools/spotify/skip_previous.ts | 2 +- apps/sim/tools/stagehand/extract.ts | 3 - apps/sim/tools/typeform/insights.ts | 3 - apps/sim/tools/wealthbox/read_contact.ts | 3 - apps/sim/tools/wealthbox/read_note.ts | 3 - apps/sim/tools/wordpress/create_category.ts | 2 +- apps/sim/tools/wordpress/create_comment.ts | 2 +- apps/sim/tools/wordpress/create_page.ts | 2 +- apps/sim/tools/wordpress/create_post.ts | 2 +- apps/sim/tools/wordpress/create_tag.ts | 2 +- apps/sim/tools/wordpress/delete_comment.ts | 2 +- apps/sim/tools/wordpress/delete_media.ts | 2 +- apps/sim/tools/wordpress/delete_page.ts | 2 +- apps/sim/tools/wordpress/delete_post.ts | 2 +- apps/sim/tools/wordpress/get_current_user.ts | 2 +- apps/sim/tools/wordpress/get_media.ts | 2 +- apps/sim/tools/wordpress/get_page.ts | 2 +- apps/sim/tools/wordpress/get_post.ts | 2 +- apps/sim/tools/wordpress/get_user.ts | 2 +- apps/sim/tools/wordpress/list_categories.ts | 2 +- apps/sim/tools/wordpress/list_comments.ts | 2 +- apps/sim/tools/wordpress/list_media.ts | 2 +- apps/sim/tools/wordpress/list_pages.ts | 2 +- apps/sim/tools/wordpress/list_posts.ts | 2 +- apps/sim/tools/wordpress/list_tags.ts | 2 +- apps/sim/tools/wordpress/list_users.ts | 2 +- apps/sim/tools/wordpress/search_content.ts | 2 +- apps/sim/tools/wordpress/update_comment.ts | 2 +- apps/sim/tools/wordpress/update_page.ts | 2 +- apps/sim/tools/wordpress/update_post.ts | 2 +- apps/sim/tools/wordpress/upload_media.ts | 11 +- .../zendesk/autocomplete_organizations.ts | 5 +- apps/sim/tools/zendesk/create_organization.ts | 6 +- .../zendesk/create_organizations_bulk.ts | 2 +- apps/sim/tools/zendesk/create_ticket.ts | 6 +- apps/sim/tools/zendesk/create_tickets_bulk.ts | 2 +- apps/sim/tools/zendesk/create_user.ts | 2 +- apps/sim/tools/zendesk/create_users_bulk.ts | 2 +- apps/sim/tools/zendesk/delete_organization.ts | 5 +- apps/sim/tools/zendesk/delete_ticket.ts | 5 +- apps/sim/tools/zendesk/delete_user.ts | 5 +- apps/sim/tools/zendesk/get_current_user.ts | 5 +- apps/sim/tools/zendesk/get_organization.ts | 9 +- apps/sim/tools/zendesk/get_organizations.ts | 5 +- apps/sim/tools/zendesk/get_ticket.ts | 9 +- apps/sim/tools/zendesk/get_tickets.ts | 5 +- apps/sim/tools/zendesk/get_user.ts | 5 +- apps/sim/tools/zendesk/get_users.ts | 5 +- apps/sim/tools/zendesk/merge_tickets.ts | 5 +- apps/sim/tools/zendesk/search.ts | 10 +- apps/sim/tools/zendesk/search_count.ts | 5 +- apps/sim/tools/zendesk/search_users.ts | 5 +- apps/sim/tools/zendesk/update_organization.ts | 6 +- apps/sim/tools/zendesk/update_ticket.ts | 6 +- apps/sim/tools/zendesk/update_tickets_bulk.ts | 5 +- apps/sim/tools/zendesk/update_user.ts | 2 +- apps/sim/tools/zendesk/update_users_bulk.ts | 2 +- biome.json | 27 +- 271 files changed, 1520 insertions(+), 1498 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table-skeleton.tsx create mode 100644 apps/sim/hooks/queries/invitations.ts delete mode 100644 apps/sim/hooks/use-slack-accounts.ts delete mode 100644 apps/sim/hooks/use-workspace-permissions.ts diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx index 216ccf841..81bbfa3a2 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/components/notifications/notifications.tsx @@ -32,8 +32,7 @@ import { useTestNotification, useUpdateNotification, } from '@/hooks/queries/notifications' -import { useConnectOAuthService } from '@/hooks/queries/oauth-connections' -import { useSlackAccounts } from '@/hooks/use-slack-accounts' +import { useConnectedAccounts, useConnectOAuthService } from '@/hooks/queries/oauth-connections' import { CORE_TRIGGER_TYPES, type CoreTriggerType } from '@/stores/logs/filters/types' import { SlackChannelSelector } from './components/slack-channel-selector' import { WorkflowSelector } from './components/workflow-selector' @@ -167,7 +166,8 @@ export function NotificationSettings({ const deleteNotification = useDeleteNotification() const testNotification = useTestNotification() - const { accounts: slackAccounts, isLoading: isLoadingSlackAccounts } = useSlackAccounts() + const { data: slackAccounts = [], isLoading: isLoadingSlackAccounts } = + useConnectedAccounts('slack') const connectSlack = useConnectOAuthService() useEffect(() => { @@ -530,7 +530,7 @@ export function NotificationSettings({ message: result.data?.error || (result.data?.success ? 'Test sent successfully' : 'Test failed'), }) - } catch (error) { + } catch (_error) { setTestStatus({ id, success: false, message: 'Failed to send test' }) } } diff --git a/apps/sim/app/workspace/[workspaceId]/providers/workspace-permissions-provider.tsx b/apps/sim/app/workspace/[workspaceId]/providers/workspace-permissions-provider.tsx index 607d17788..ab198204d 100644 --- a/apps/sim/app/workspace/[workspaceId]/providers/workspace-permissions-provider.tsx +++ b/apps/sim/app/workspace/[workspaceId]/providers/workspace-permissions-provider.tsx @@ -1,32 +1,28 @@ 'use client' import type React from 'react' -import { createContext, useContext, useEffect, useMemo, useState } from 'react' +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' +import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'next/navigation' -import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions' import { - useWorkspacePermissions, + useWorkspacePermissionsQuery, type WorkspacePermissions, -} from '@/hooks/use-workspace-permissions' + workspaceKeys, +} from '@/hooks/queries/workspace' +import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions' import { useNotificationStore } from '@/stores/notifications' import { useOperationQueueStore } from '@/stores/operation-queue/store' const logger = createLogger('WorkspacePermissionsProvider') interface WorkspacePermissionsContextType { - // Raw workspace permissions data workspacePermissions: WorkspacePermissions | null permissionsLoading: boolean permissionsError: string | null updatePermissions: (newPermissions: WorkspacePermissions) => void refetchPermissions: () => Promise - - // Computed user permissions (connection-aware) userPermissions: WorkspaceUserPermissions & { isOfflineMode?: boolean } - - // Connection state management - setOfflineMode: (isOffline: boolean) => void } const WorkspacePermissionsContext = createContext({ @@ -43,7 +39,6 @@ const WorkspacePermissionsContext = createContext {}, }) interface WorkspacePermissionsProviderProps { @@ -51,35 +46,20 @@ interface WorkspacePermissionsProviderProps { } /** - * Provider that manages workspace permissions and user access - * Also provides connection-aware permissions that enforce read-only mode when offline + * Provides workspace permissions and connection-aware user access throughout the app. + * Enforces read-only mode when offline to prevent data loss. */ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsProviderProps) { const params = useParams() const workspaceId = params?.workspaceId as string + const queryClient = useQueryClient() - // Manage offline mode state locally - const [isOfflineMode, setIsOfflineMode] = useState(false) - - // Track whether we've already surfaced an offline notification to avoid duplicates const [hasShownOfflineNotification, setHasShownOfflineNotification] = useState(false) - - // Get operation error state directly from the store (avoid full useCollaborativeWorkflow subscription) const hasOperationError = useOperationQueueStore((state) => state.hasOperationError) - const addNotification = useNotificationStore((state) => state.addNotification) - // Set offline mode when there are operation errors - useEffect(() => { - if (hasOperationError) { - setIsOfflineMode(true) - } - }, [hasOperationError]) + const isOfflineMode = hasOperationError - /** - * Surface a global notification when entering offline mode. - * Uses the shared notifications system instead of bespoke UI in individual components. - */ useEffect(() => { if (!isOfflineMode || hasShownOfflineNotification) { return @@ -89,7 +69,6 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP addNotification({ level: 'error', message: 'Connection unavailable', - // Global notification (no workflowId) so it is visible regardless of the active workflow action: { type: 'refresh', message: '', @@ -101,40 +80,44 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP } }, [addNotification, hasShownOfflineNotification, isOfflineMode]) - // Fetch workspace permissions and loading state const { - permissions: workspacePermissions, - loading: permissionsLoading, - error: permissionsError, - updatePermissions, - refetch: refetchPermissions, - } = useWorkspacePermissions(workspaceId) + data: workspacePermissions, + isLoading: permissionsLoading, + error: permissionsErrorObj, + refetch, + } = useWorkspacePermissionsQuery(workspaceId) + + const permissionsError = permissionsErrorObj?.message ?? null + + const updatePermissions = useCallback( + (newPermissions: WorkspacePermissions) => { + if (!workspaceId) return + queryClient.setQueryData(workspaceKeys.permissions(workspaceId), newPermissions) + }, + [workspaceId, queryClient] + ) + + const refetchPermissions = useCallback(async () => { + await refetch() + }, [refetch]) - // Get base user permissions from workspace permissions const baseUserPermissions = useUserPermissions( - workspacePermissions, + workspacePermissions ?? null, permissionsLoading, permissionsError ) - // Note: Connection-based error detection removed - only rely on operation timeouts - // The 5-second operation timeout system will handle all error cases - - // Create connection-aware permissions that override user permissions when offline const userPermissions = useMemo((): WorkspaceUserPermissions & { isOfflineMode?: boolean } => { if (isOfflineMode) { - // In offline mode, force read-only permissions regardless of actual user permissions return { ...baseUserPermissions, canEdit: false, canAdmin: false, - // Keep canRead true so users can still view content canRead: baseUserPermissions.canRead, isOfflineMode: true, } } - // When online, use normal permissions return { ...baseUserPermissions, isOfflineMode: false, @@ -143,13 +126,12 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP const contextValue = useMemo( () => ({ - workspacePermissions, + workspacePermissions: workspacePermissions ?? null, permissionsLoading, permissionsError, updatePermissions, refetchPermissions, userPermissions, - setOfflineMode: setIsOfflineMode, }), [ workspacePermissions, @@ -169,8 +151,8 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP } /** - * Hook to access workspace permissions and data from context - * This provides both raw workspace permissions and computed user permissions + * Accesses workspace permissions data and operations from context. + * Must be used within a WorkspacePermissionsProvider. */ export function useWorkspacePermissionsContext(): WorkspacePermissionsContextType { const context = useContext(WorkspacePermissionsContext) @@ -183,8 +165,8 @@ export function useWorkspacePermissionsContext(): WorkspacePermissionsContextTyp } /** - * Hook to access user permissions from context - * This replaces individual useUserPermissions calls and includes connection-aware permissions + * Accesses the current user's computed permissions including offline mode status. + * Convenience hook that extracts userPermissions from the context. */ export function useUserPermissionsContext(): WorkspaceUserPermissions & { isOfflineMode?: boolean diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/files/files.tsx index 824dbf026..92422d6ee 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/files/files.tsx @@ -21,14 +21,13 @@ import { cn } from '@/lib/core/utils/cn' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace' import { getFileExtension } from '@/lib/uploads/utils/file-utils' import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components' +import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { useDeleteWorkspaceFile, useStorageInfo, useUploadWorkspaceFile, useWorkspaceFiles, } from '@/hooks/queries/workspace-files' -import { useUserPermissions } from '@/hooks/use-user-permissions' -import { useWorkspacePermissions } from '@/hooks/use-workspace-permissions' const logger = createLogger('FileUploadsSettings') const isBillingEnabled = isTruthy(getEnv('NEXT_PUBLIC_BILLING_ENABLED')) @@ -94,9 +93,7 @@ export function Files() { const fileInputRef = useRef(null) const scrollContainerRef = useRef(null) - const { permissions: workspacePermissions, loading: permissionsLoading } = - useWorkspacePermissions(workspaceId) - const userPermissions = useUserPermissions(workspacePermissions, permissionsLoading) + const { userPermissions, permissionsLoading } = useWorkspacePermissionsContext() const handleUploadClick = () => { fileInputRef.current?.click() diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table-skeleton.tsx deleted file mode 100644 index 0e48939bd..000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table-skeleton.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import { Skeleton } from '@/components/ui/skeleton' - -export const PermissionsTableSkeleton = React.memo(() => ( -
-
-
-
- -
-
-
-
- - - -
-
-
-
-)) - -PermissionsTableSkeleton.displayName = 'PermissionsTableSkeleton' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table.tsx index b27970559..9b313c266 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table.tsx @@ -1,20 +1,39 @@ import { useEffect, useMemo, useState } from 'react' import { Loader2, RotateCw, X } from 'lucide-react' import { Badge, Button, Tooltip } from '@/components/emcn' +import { Skeleton } from '@/components/ui/skeleton' import { useSession } from '@/lib/auth/auth-client' import type { PermissionType } from '@/lib/workspaces/permissions/utils' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' -import type { WorkspacePermissions } from '@/hooks/use-workspace-permissions' +import type { WorkspacePermissions } from '@/hooks/queries/workspace' import { PermissionSelector } from './permission-selector' -import { PermissionsTableSkeleton } from './permissions-table-skeleton' import type { UserPermissions } from './types' +const PermissionsTableSkeleton = () => ( +
+
+
+
+ +
+
+
+
+ + + +
+
+
+
+) + export interface PermissionsTableProps { userPermissions: UserPermissions[] onPermissionChange: (userId: string, permissionType: PermissionType) => void onRemoveMember?: (userId: string, email: string) => void onRemoveInvitation?: (invitationId: string, email: string) => void - onResendInvitation?: (invitationId: string, email: string) => void + onResendInvitation?: (invitationId: string) => void disabled?: boolean existingUserPermissionChanges: Record> isSaving?: boolean @@ -143,7 +162,6 @@ export const PermissionsTable = ({
{allUsers.map((user) => { const isCurrentUser = user.isCurrentUser === true - const isExistingUser = filteredExistingUsers.some((eu) => eu.email === user.email) const isPendingInvitation = user.isPendingInvitation === true const userIdentifier = user.userId || user.email const originalPermission = workspacePermissions?.users?.find( @@ -205,7 +223,7 @@ export const PermissionsTable = ({ - -
- )} +
+ + +