Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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' })
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void>

// Computed user permissions (connection-aware)
userPermissions: WorkspaceUserPermissions & { isOfflineMode?: boolean }

// Connection state management
setOfflineMode: (isOffline: boolean) => void
}

const WorkspacePermissionsContext = createContext<WorkspacePermissionsContextType>({
Expand All @@ -43,43 +39,27 @@ const WorkspacePermissionsContext = createContext<WorkspacePermissionsContextTyp
isLoading: false,
error: null,
},
setOfflineMode: () => {},
})

interface WorkspacePermissionsProviderProps {
children: React.ReactNode
}

/**
* 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
Expand All @@ -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: '',
Expand All @@ -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)

// Get base user permissions from workspace permissions
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])

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,
Expand All @@ -143,13 +126,12 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP

const contextValue = useMemo(
() => ({
workspacePermissions,
workspacePermissions: workspacePermissions ?? null,
permissionsLoading,
permissionsError,
updatePermissions,
refetchPermissions,
userPermissions,
setOfflineMode: setIsOfflineMode,
}),
[
workspacePermissions,
Expand All @@ -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)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down Expand Up @@ -94,9 +93,7 @@ export function Files() {
const fileInputRef = useRef<HTMLInputElement>(null)
const scrollContainerRef = useRef<HTMLDivElement>(null)

const { permissions: workspacePermissions, loading: permissionsLoading } =
useWorkspacePermissions(workspaceId)
const userPermissions = useUserPermissions(workspacePermissions, permissionsLoading)
const { userPermissions, permissionsLoading } = useWorkspacePermissionsContext()

const handleUploadClick = () => {
fileInputRef.current?.click()
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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 = () => (
<div className='scrollbar-hide max-h-[300px] overflow-y-auto'>
<div className='flex items-center justify-between gap-[8px] py-[8px]'>
<div className='min-w-0 flex-1'>
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[14px] w-40 rounded-[4px]' />
</div>
</div>
<div className='flex flex-shrink-0 items-center'>
<div className='inline-flex gap-[2px]'>
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
<Skeleton className='h-[28px] w-[44px] rounded-[5px]' />
</div>
</div>
</div>
</div>
)

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<string, Partial<UserPermissions>>
isSaving?: boolean
Expand Down Expand Up @@ -143,7 +162,6 @@ export const PermissionsTable = ({
<div>
{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(
Expand Down Expand Up @@ -205,7 +223,7 @@ export const PermissionsTable = ({
<span className='inline-flex'>
<Button
variant='ghost'
onClick={() => onResendInvitation(user.invitationId!, user.email)}
onClick={() => onResendInvitation(user.invitationId!)}
disabled={
disabled ||
isSaving ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { PermissionSelector } from './components/permission-selector'
export { PermissionsTable } from './components/permissions-table'
export { PermissionsTableSkeleton } from './components/permissions-table-skeleton'
export type { PermissionType, UserPermissions } from './components/types'
export { InviteModal } from './invite-modal'
Loading