From 30d6b703461b7764f5e9eba47f10c6151e90b280 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 13 Apr 2026 14:13:10 -0700 Subject: [PATCH 01/11] feat(workspaces): add workspace logo upload --- apps/sim/app/api/files/authorization.ts | 8 +- .../app/api/files/serve/[...path]/route.ts | 4 +- apps/sim/app/api/files/upload/route.ts | 11 +- apps/sim/app/api/workspaces/[id]/route.ts | 11 +- .../knowledge-header/knowledge-header.tsx | 7 +- .../hooks/use-profile-picture-upload.ts | 7 +- .../components/context-menu/context-menu.tsx | 42 +- .../workspace-header/workspace-header.tsx | 86 +- .../w/components/sidebar/hooks/index.ts | 3 +- .../hooks/use-workspace-logo-upload.ts | 146 + .../sidebar/hooks/use-workspace-management.ts | 5 +- .../w/components/sidebar/sidebar.tsx | 42 +- .../components/whitelabeling-settings.tsx | 2 + apps/sim/hooks/queries/workspace.ts | 2 + apps/sim/lib/core/config/env.ts | 2 + apps/sim/lib/uploads/config.ts | 25 + apps/sim/lib/uploads/shared/types.ts | 1 + apps/sim/lib/uploads/utils/file-utils.ts | 3 +- helm/sim/examples/values-aws.yaml | 1 + helm/sim/examples/values-azure.yaml | 1 + helm/sim/values.yaml | 2 + .../db/migrations/0190_shocking_karma.sql | 1 + .../db/migrations/meta/0190_snapshot.json | 15379 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 9 +- packages/db/schema.ts | 1 + 25 files changed, 15760 insertions(+), 41 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-logo-upload.ts create mode 100644 packages/db/migrations/0190_shocking_karma.sql create mode 100644 packages/db/migrations/meta/0190_snapshot.json diff --git a/apps/sim/app/api/files/authorization.ts b/apps/sim/app/api/files/authorization.ts index 1be57e4a389..e9938c14940 100644 --- a/apps/sim/app/api/files/authorization.ts +++ b/apps/sim/app/api/files/authorization.ts @@ -114,8 +114,12 @@ export async function verifyFileAccess( // Infer context from key if not explicitly provided const inferredContext = context || inferContextFromKey(cloudKey) - // 0. Public contexts: profile pictures and OG images are publicly accessible - if (inferredContext === 'profile-pictures' || inferredContext === 'og-images') { + // 0. Public contexts: profile pictures, OG images, and workspace logos are publicly accessible + if ( + inferredContext === 'profile-pictures' || + inferredContext === 'og-images' || + inferredContext === 'workspace-logos' + ) { logger.info('Public file access allowed', { cloudKey, context: inferredContext }) return true } diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index bc14086395a..6afa0f7abd5 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -95,7 +95,9 @@ export async function GET( const cloudKey = isCloudPath ? path.slice(1).join('/') : fullPath const isPublicByKeyPrefix = - cloudKey.startsWith('profile-pictures/') || cloudKey.startsWith('og-images/') + cloudKey.startsWith('profile-pictures/') || + cloudKey.startsWith('og-images/') || + cloudKey.startsWith('workspace-logos/') if (isPublicByKeyPrefix) { const context = inferContextFromKey(cloudKey) diff --git a/apps/sim/app/api/files/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index b6791a3841b..129fb4f7e8b 100644 --- a/apps/sim/app/api/files/upload/route.ts +++ b/apps/sim/app/api/files/upload/route.ts @@ -64,7 +64,7 @@ export async function POST(request: NextRequest) { // Context must be explicitly provided if (!contextParam) { throw new InvalidRequestError( - 'Upload requires explicit context parameter (knowledge-base, workspace, execution, copilot, chat, or profile-pictures)' + 'Upload requires explicit context parameter (knowledge-base, workspace, execution, copilot, chat, profile-pictures, or workspace-logos)' ) } @@ -282,7 +282,12 @@ export async function POST(request: NextRequest) { continue } - if (context === 'copilot' || context === 'chat' || context === 'profile-pictures') { + if ( + context === 'copilot' || + context === 'chat' || + context === 'profile-pictures' || + context === 'workspace-logos' + ) { if (context !== 'copilot' && !isImageFileType(file.type)) { throw new InvalidRequestError( `Only image files (JPEG, PNG, GIF, WebP, SVG) are allowed for ${context} uploads` @@ -352,7 +357,7 @@ export async function POST(request: NextRequest) { // Unknown context throw new InvalidRequestError( - `Unsupported context: ${context}. Use knowledge-base, workspace, execution, copilot, chat, or profile-pictures` + `Unsupported context: ${context}. Use knowledge-base, workspace, execution, copilot, chat, profile-pictures, or workspace-logos` ) } diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts index d051b1fffcb..b7da33bd7df 100644 --- a/apps/sim/app/api/workspaces/[id]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/route.ts @@ -20,6 +20,7 @@ const patchWorkspaceSchema = z.object({ .string() .regex(/^#[0-9a-fA-F]{6}$/) .optional(), + logoUrl: z.string().min(1).nullable().optional(), billedAccountUserId: z.string().optional(), allowPersonalApiKeys: z.boolean().optional(), }) @@ -119,11 +120,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< try { const body = patchWorkspaceSchema.parse(await request.json()) - const { name, color, billedAccountUserId, allowPersonalApiKeys } = body + const { name, color, logoUrl, billedAccountUserId, allowPersonalApiKeys } = body if ( name === undefined && color === undefined && + logoUrl === undefined && billedAccountUserId === undefined && allowPersonalApiKeys === undefined ) { @@ -150,6 +152,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< updateData.color = color } + if (logoUrl !== undefined) { + updateData.logoUrl = logoUrl + } + if (allowPersonalApiKeys !== undefined) { updateData.allowPersonalApiKeys = Boolean(allowPersonalApiKeys) } @@ -216,6 +222,9 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< changes: { ...(name !== undefined && { name: { from: existingWorkspace.name, to: name } }), ...(color !== undefined && { color: { from: existingWorkspace.color, to: color } }), + ...(logoUrl !== undefined && { + logoUrl: { from: existingWorkspace.logoUrl, to: logoUrl }, + }), ...(allowPersonalApiKeys !== undefined && { allowPersonalApiKeys: { from: existingWorkspace.allowPersonalApiKeys, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx index c5880735309..77c29127dfa 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-header/knowledge-header.tsx @@ -17,6 +17,7 @@ import { ChevronDown } from '@/components/emcn/icons' import { Trash } from '@/components/emcn/icons/trash' import { filterButtonClass } from '@/app/workspace/[workspaceId]/knowledge/components/constants' import { useUpdateKnowledgeBase } from '@/hooks/queries/kb/knowledge' +import type { Workspace } from '@/hooks/queries/workspace' const logger = createLogger('KnowledgeHeader') @@ -48,12 +49,6 @@ interface KnowledgeHeaderProps { options?: KnowledgeHeaderOptions } -interface Workspace { - id: string - name: string - permissions: 'admin' | 'write' | 'read' -} - export function KnowledgeHeader({ breadcrumbs, options }: KnowledgeHeaderProps) { const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false) const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(false) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts b/apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts index 04078386354..07213f77d7a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' +import type { StorageContext } from '@/lib/uploads/shared/types' const logger = createLogger('ProfilePictureUpload') const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB @@ -9,6 +10,7 @@ interface UseProfilePictureUploadProps { onUpload?: (url: string | null) => void onError?: (error: string) => void currentImage?: string | null + context?: StorageContext } /** @@ -19,6 +21,7 @@ export function useProfilePictureUpload({ onUpload, onError, currentImage, + context = 'profile-pictures', }: UseProfilePictureUploadProps = {}) { const previewRef = useRef(null) const fileInputRef = useRef(null) @@ -52,7 +55,7 @@ export function useProfilePictureUpload({ try { const formData = new FormData() formData.append('file', file) - formData.append('context', 'profile-pictures') + formData.append('context', context) const response = await fetch('/api/files/upload', { method: 'POST', @@ -71,7 +74,7 @@ export function useProfilePictureUpload({ } catch (error) { throw new Error(error instanceof Error ? error.message : 'Failed to upload profile picture') } - }, []) + }, [context]) const processFile = useCallback( async (file: File) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index 31a9eb854a9..9d1c2cfde7f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -27,6 +27,7 @@ import { Trash, Unlock, Upload, + X, } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' import { WORKFLOW_COLORS } from '@/lib/workflows/colors' @@ -267,6 +268,12 @@ interface ContextMenuProps { disableLock?: boolean isLocked?: boolean showDelete?: boolean + onUploadLogo?: () => void + onRemoveLogo?: () => void + showUploadLogo?: boolean + showRemoveLogo?: boolean + disableUploadLogo?: boolean + disableRemoveLogo?: boolean } /** @@ -315,6 +322,12 @@ export function ContextMenu({ disableLock = false, isLocked = false, showDelete = true, + onUploadLogo, + onRemoveLogo, + showUploadLogo = false, + showRemoveLogo = false, + disableUploadLogo = false, + disableRemoveLogo = false, }: ContextMenuProps) { const [hexInput, setHexInput] = useState(currentColor || '#ffffff') @@ -368,7 +381,9 @@ export function ContextMenu({ (showCreate && onCreate) || (showCreateFolder && onCreateFolder) || (showColorChange && onColorChange) || - (showLock && onToggleLock) + (showLock && onToggleLock) || + (showUploadLogo && onUploadLogo) || + (showRemoveLogo && onRemoveLogo) const hasCopySection = (showDuplicate && onDuplicate) || (showExport && onExport) return ( @@ -484,6 +499,31 @@ export function ContextMenu({ /> )} + {showUploadLogo && onUploadLogo && ( + { + onUploadLogo() + onClose() + }} + > + + Upload logo + + )} + {showRemoveLogo && onRemoveLogo && ( + { + onRemoveLogo() + onClose() + }} + > + + Remove logo + + )} + {showLock && onToggleLock && ( Promise + /** Callback to upload a workspace logo */ + onUploadLogo?: (workspaceId: string) => void + /** Callback to remove the workspace logo */ + onRemoveLogo?: (workspaceId: string) => Promise /** Callback to leave the workspace */ onLeaveWorkspace?: (workspaceId: string) => Promise /** Whether workspace leave is in progress */ @@ -100,6 +104,8 @@ export function WorkspaceHeader({ onImportWorkspace, isImportingWorkspace, onColorChange, + onUploadLogo, + onRemoveLogo, onLeaveWorkspace, isLeavingWorkspace, sessionUserId, @@ -286,6 +292,16 @@ export function WorkspaceHeader({ await onColorChange(capturedWorkspaceRef.current.id, color) } + const handleUploadLogoAction = () => { + if (!capturedWorkspaceRef.current || !onUploadLogo) return + onUploadLogo(capturedWorkspaceRef.current.id) + } + + const handleRemoveLogoAction = async () => { + if (!capturedWorkspaceRef.current || !onRemoveLogo) return + await onRemoveLogo(capturedWorkspaceRef.current.id) + } + /** * Handle leave workspace after confirmation */ @@ -348,12 +364,20 @@ export function WorkspaceHeader({ }} > {activeWorkspaceFull ? ( -
- {workspaceInitial} -
+ activeWorkspaceFull.logoUrl ? ( + {activeWorkspaceFull.name + ) : ( +
+ {workspaceInitial} +
+ ) ) : ( )} @@ -394,14 +418,22 @@ export function WorkspaceHeader({ <>
{activeWorkspaceFull ? ( -
- {workspaceInitial} -
+ activeWorkspaceFull.logoUrl ? ( + {activeWorkspaceFull.name + ) : ( +
+ {workspaceInitial} +
+ ) ) : ( )} @@ -578,12 +610,20 @@ export function WorkspaceHeader({ disabled > {activeWorkspaceFull ? ( -
- {workspaceInitial} -
+ activeWorkspaceFull.logoUrl ? ( + {activeWorkspaceFull.name + ) : ( +
+ {workspaceInitial} +
+ ) ) : ( )} @@ -619,17 +659,23 @@ export function WorkspaceHeader({ onDelete={handleDeleteAction} onLeave={handleLeaveAction} onColorChange={onColorChange ? handleColorChangeAction : undefined} + onUploadLogo={onUploadLogo ? handleUploadLogoAction : undefined} + onRemoveLogo={onRemoveLogo ? handleRemoveLogoAction : undefined} currentColor={capturedWorkspace?.color} showRename={true} showDuplicate={true} showExport={true} showColorChange={!!onColorChange} + showUploadLogo={!!onUploadLogo} + showRemoveLogo={!!onRemoveLogo && !!capturedWorkspace?.logoUrl} showLeave={!isOwner && !!onLeaveWorkspace} disableRename={!contextCanAdmin} disableDuplicate={!contextCanEdit} disableExport={!contextCanAdmin} disableDelete={!contextCanAdmin || workspaces.length <= 1} disableColorChange={!contextCanAdmin} + disableUploadLogo={!contextCanAdmin} + disableRemoveLogo={!contextCanAdmin} /> ) })()} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts index 62066f471bf..dd02ce57b80 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts @@ -17,4 +17,5 @@ export { useSidebarResize } from './use-sidebar-resize' export { useTaskSelection } from './use-task-selection' export { useWorkflowOperations } from './use-workflow-operations' export { useWorkflowSelection } from './use-workflow-selection' -export { useWorkspaceManagement } from './use-workspace-management' +export { useWorkspaceLogoUpload } from './use-workspace-logo-upload' +export { useWorkspaceManagement, type Workspace } from './use-workspace-management' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-logo-upload.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-logo-upload.ts new file mode 100644 index 00000000000..73d530de1a3 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-logo-upload.ts @@ -0,0 +1,146 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { createLogger } from '@sim/logger' + +const logger = createLogger('WorkspaceLogoUpload') +const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB +const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml', 'image/webp'] + +interface UseWorkspaceLogoUploadProps { + currentLogoUrl?: string | null + onUpload?: (url: string | null) => void + onError?: (error: string) => void +} + +/** + * Hook for handling workspace logo upload functionality. + * Manages file validation, preview generation, and server upload. + */ +export function useWorkspaceLogoUpload({ + currentLogoUrl, + onUpload, + onError, +}: UseWorkspaceLogoUploadProps = {}) { + const previewRef = useRef(null) + const fileInputRef = useRef(null) + const onUploadRef = useRef(onUpload) + const onErrorRef = useRef(onError) + const currentLogoUrlRef = useRef(currentLogoUrl) + const [previewUrl, setPreviewUrl] = useState(currentLogoUrl || null) + const [isUploading, setIsUploading] = useState(false) + + useEffect(() => { + onUploadRef.current = onUpload + onErrorRef.current = onError + currentLogoUrlRef.current = currentLogoUrl + }, [onUpload, onError, currentLogoUrl]) + + useEffect(() => { + if (previewRef.current && previewRef.current !== currentLogoUrl) { + URL.revokeObjectURL(previewRef.current) + previewRef.current = null + } + setPreviewUrl(currentLogoUrl || null) + }, [currentLogoUrl]) + + const validateFile = useCallback((file: File): string | null => { + if (file.size > MAX_FILE_SIZE) { + return `File "${file.name}" is too large. Maximum size is 5MB.` + } + if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { + return `File "${file.name}" is not a supported image format. Please use PNG, JPEG, SVG, or WebP.` + } + return null + }, []) + + const uploadFileToServer = useCallback(async (file: File): Promise => { + const formData = new FormData() + formData.append('file', file) + formData.append('context', 'workspace-logos') + + const response = await fetch('/api/files/upload', { + method: 'POST', + body: formData, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: response.statusText })) + throw new Error(errorData.error || `Failed to upload file: ${response.status}`) + } + + const data = await response.json() + const publicUrl = data.fileInfo?.path || data.path || data.url + logger.info(`Workspace logo uploaded successfully: ${publicUrl}`) + return publicUrl + }, []) + + const processFile = useCallback( + async (file: File) => { + const validationError = validateFile(file) + if (validationError) { + onErrorRef.current?.(validationError) + return + } + + const newPreviewUrl = URL.createObjectURL(file) + if (previewRef.current) URL.revokeObjectURL(previewRef.current) + setPreviewUrl(newPreviewUrl) + previewRef.current = newPreviewUrl + + setIsUploading(true) + try { + const serverUrl = await uploadFileToServer(file) + URL.revokeObjectURL(newPreviewUrl) + previewRef.current = null + setPreviewUrl(serverUrl) + onUploadRef.current?.(serverUrl) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to upload workspace logo' + onErrorRef.current?.(errorMessage) + URL.revokeObjectURL(newPreviewUrl) + previewRef.current = null + setPreviewUrl(currentLogoUrlRef.current || null) + } finally { + setIsUploading(false) + } + }, + [uploadFileToServer, validateFile] + ) + + const handleFileChange = useCallback( + (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) processFile(file) + if (event.target) event.target.value = '' + }, + [processFile] + ) + + const handleRemove = useCallback(() => { + if (previewRef.current) { + URL.revokeObjectURL(previewRef.current) + previewRef.current = null + } + setPreviewUrl(null) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + onUploadRef.current?.(null) + }, []) + + useEffect(() => { + return () => { + if (previewRef.current) { + URL.revokeObjectURL(previewRef.current) + } + } + }, []) + + return { + previewUrl, + fileInputRef, + handleFileChange, + handleRemove, + isUploading, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts index 7a28da07ab4..c1de8ef85f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts @@ -127,7 +127,10 @@ export function useWorkspaceManagement({ }, [workspaces, isWorkspacesLoading, isWorkspacesFetching]) const updateWorkspace = useCallback( - async (workspaceId: string, updates: { name?: string; color?: string }): Promise => { + async ( + workspaceId: string, + updates: { name?: string; color?: string; logoUrl?: string | null } + ): Promise => { try { await updateWorkspaceMutation.mutateAsync({ workspaceId, ...updates }) logger.info('Successfully updated workspace:', updates) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 258d09f4c38..13833167478 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -71,7 +71,9 @@ import { useSidebarResize, useTaskSelection, useWorkflowOperations, + useWorkspaceLogoUpload, useWorkspaceManagement, + type Workspace, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { createSidebarDragGhost, @@ -460,6 +462,20 @@ export const Sidebar = memo(function Sidebar() { sessionUserId: sessionData?.user?.id, }) + const activeWorkspaceFull = workspaces.find((w) => w.id === workspaceId) + const logoTargetWorkspaceIdRef = useRef(workspaceId) + + const { fileInputRef: logoFileInputRef, handleFileChange: handleLogoFileChange } = + useWorkspaceLogoUpload({ + currentLogoUrl: activeWorkspaceFull?.logoUrl, + onUpload: (url) => { + updateWorkspace(logoTargetWorkspaceIdRef.current, { logoUrl: url }) + }, + onError: (error) => { + logger.error('Workspace logo upload error:', error) + }, + }) + const { handleMouseDown, isResizing } = useSidebarResize() const { @@ -980,7 +996,7 @@ export const Sidebar = memo(function Sidebar() { }, []) const handleWorkspaceSwitch = useCallback( - async (workspace: { id: string; name: string; ownerId: string; role?: string }) => { + async (workspace: Workspace) => { if (workspace.id === workspaceId) { setIsWorkspaceMenuOpen(false) return @@ -1017,6 +1033,21 @@ export const Sidebar = memo(function Sidebar() { [updateWorkspace] ) + const handleUploadLogo = useCallback( + (workspaceIdToUpdate: string) => { + logoTargetWorkspaceIdRef.current = workspaceIdToUpdate + logoFileInputRef.current?.click() + }, + [logoFileInputRef] + ) + + const handleRemoveLogo = useCallback( + async (workspaceIdToUpdate: string) => { + await updateWorkspace(workspaceIdToUpdate, { logoUrl: null }) + }, + [updateWorkspace] + ) + const handleDeleteWorkspace = useCallback( async (workspaceIdToDelete: string) => { const workspaceToDelete = workspaces.find((w) => w.id === workspaceIdToDelete) @@ -1232,6 +1263,13 @@ export const Sidebar = memo(function Sidebar() { return ( <> +