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 @@ -29,7 +29,9 @@ import {
interface ChatRowProps {
chat: OrganizationChat
isCurrentRoute: boolean
isSelected: boolean
isMenuOpen: boolean
onSelectChat: (chatId: string, shiftKey: boolean) => void
onContextMenu: (e: React.MouseEvent, chatId: string) => void
onMorePointerDown: () => void
onMoreClick: (e: React.MouseEvent<HTMLButtonElement>, chatId: string) => void
Expand All @@ -38,7 +40,9 @@ interface ChatRowProps {
function ChatRow({
chat,
isCurrentRoute,
isSelected,
isMenuOpen,
onSelectChat,
onContextMenu,
onMorePointerDown,
onMoreClick,
Expand All @@ -56,10 +60,11 @@ function ChatRow({
chatId={chat.id}
isCurrentRoute={isCurrentRoute}
className={cn(
chipVariants({ active: isCurrentRoute || isMenuOpen, fullWidth: true }),
chipVariants({ active: isCurrentRoute || isSelected || isMenuOpen, fullWidth: true }),
rowActionsGroupClass
)}
onContextMenu={(e) => onContextMenu(e, chat.id)}
onSelectChat={onSelectChat}
>
<OverflowText label={chat.name} className='flex-1 text-[var(--text-body)]' />
<RowActions
Expand Down Expand Up @@ -108,8 +113,7 @@ export function ChatsSection({
pathname,
}: ChatsSectionProps) {
const actions = useOrganizationChatActions({ organizationId, chats })
const { menu, hover, rename, selectedChat } = actions
const menuOpenChatId = menu.isOpen ? selectedChat?.id : null
const { menu, hover, rename, selectedChat, selectedChats, menuOpenChatId } = actions
const saveRename = () => {
void rename.saveRename()
}
Expand Down Expand Up @@ -137,6 +141,8 @@ export function ChatsSection({
key={chat.id}
chat={chat}
isCurrentRoute={pathname === chat.href}
isSelected={selectedChats.size > 1 && selectedChats.has(chat.id)}
onSelectChat={actions.handleChatClick}
isMenuOpen={menuOpenChatId === chat.id}
isEditing={rename.editingId === chat.id}
editValue={rename.value}
Expand Down Expand Up @@ -179,7 +185,9 @@ export function ChatsSection({
key={chat.id}
chat={chat}
isCurrentRoute={pathname === chat.href}
isSelected={selectedChats.size > 1 && selectedChats.has(chat.id)}
isMenuOpen={menuOpenChatId === chat.id}
onSelectChat={actions.handleChatClick}
Comment thread
waleedlatif1 marked this conversation as resolved.
onContextMenu={actions.onContextMenu}
onMorePointerDown={actions.onMorePointerDown}
onMoreClick={actions.onMoreClick}
Expand All @@ -197,29 +205,30 @@ export function ChatsSection({
menuRef={menu.menuRef}
onClose={menu.closeMenu}
onOpenInNewTab={actions.openInNewTab}
onCopyLink={actions.copyLink}
onCopyLink={selectedChat ? actions.copyLink : undefined}
onRename={actions.startRename}
renameInputRef={rename.inputRef}
onTogglePin={actions.togglePin}
onMarkAsRead={actions.markRead}
onMarkAsUnread={actions.markUnread}
showOpenInNewTab
showOpenInNewTab={Boolean(selectedChat)}
showRename={Boolean(selectedChat)}
showPin={Boolean(selectedChat)}
isPinned={Boolean(selectedChat?.isPinned)}
showMarkAsRead={Boolean(selectedChat?.isUnread)}
showMarkAsUnread={Boolean(selectedChat) && !selectedChat?.isUnread}
onDelete={actions.startDelete}
showDelete={Boolean(selectedChat)}
showDelete={actions.selectedCount > 0}
selectedCount={actions.selectedCount}
showDuplicate={false}
/>
<DeleteModal
isOpen={actions.chatToDelete !== null}
isOpen={actions.chatsToDelete.length > 0}
onClose={actions.cancelDelete}
onConfirm={actions.confirmDelete}
isDeleting={actions.isDeleting}
itemType='task'
itemName={actions.chatToDelete?.name}
itemName={actions.chatsToDelete.map((chat) => chat.name)}
/>
</>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { toast } from '@sim/emcn'
import { getErrorMessage } from '@sim/utils/errors'
import { useRouter } from 'next/navigation'
import { organizationRoutes } from '@/lib/navigation/paths'
import type { OrganizationChat } from '@/app/o/[organizationId]/components/organization-sidebar/hooks/use-organization-chats'
import { useChatSelection } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-chat-selection'
import { useFlyoutInlineRename } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-flyout-inline-rename'
import { useHoverMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu'
import {
MothershipChatDeleteError,
useDeleteMothershipChat,
useDeleteMothershipChats,
useMarkMothershipChatRead,
useMarkMothershipChatUnread,
useRenameMothershipChat,
useSetMothershipChatPinned,
} from '@/hooks/queries/mothership-chats'
import { useContextMenu } from '@/hooks/use-context-menu'
import { useFolderStore } from '@/stores/folders/store'

interface UseOrganizationChatActionsProps {
organizationId: string
Expand All @@ -26,16 +30,28 @@ export function useOrganizationChatActions({
}: UseOrganizationChatActionsProps) {
const router = useRouter()
const owner = { organizationId }
const { mutate: deleteChat, isPending: isDeleting } = useDeleteMothershipChat(owner)
const deleteChat = useDeleteMothershipChat(owner)
const deleteChats = useDeleteMothershipChats(owner)
const isDeleting = deleteChat.isPending || deleteChats.isPending
const { mutateAsync: renameChat } = useRenameMothershipChat(owner)
const { mutate: pinChat } = useSetMothershipChatPinned(owner)
const { mutate: readChat } = useMarkMothershipChatRead(owner)
const { mutate: unreadChat } = useMarkMothershipChatUnread(owner)
const menu = useContextMenu()
const hover = useHoverMenu()
const [selectedChatId, setSelectedChatId] = useState<string | null>(null)
const [chatToDelete, setChatToDelete] = useState<OrganizationChat | null>(null)
const selectedChat = chats.find((chat) => chat.id === selectedChatId)
const chatIds = useMemo(() => chats.map((chat) => chat.id), [chats])
const { selectedChats, handleChatClick } = useChatSelection({ chatIds })
const [menuSelection, setMenuSelection] = useState<{
chatId: string
chatIds: string[]
} | null>(null)
const [chatIdsToDelete, setChatIdsToDelete] = useState<string[]>([])
const deleteTargetIds = new Set(chatIdsToDelete)
const chatsToDelete = chats.filter((chat) => deleteTargetIds.has(chat.id))
const selectedChat =
menuSelection?.chatIds.length === 1
? chats.find((chat) => chat.id === menuSelection.chatId)
: undefined
const rename = useFlyoutInlineRename({
itemType: 'chat',
onSave: async (chatId, title) => {
Expand All @@ -53,14 +69,27 @@ export function useOrganizationChatActions({
hover.setLocked(menu.isOpen || rename.editingId !== null)
}, [menu.isOpen, rename.editingId, hover.setLocked])

const captureSelection = useCallback(
(chatId: string) => {
const { selectedChats, selectChatOnly } = useFolderStore.getState()
if (selectedChats.has(chatId)) {
setMenuSelection({ chatId, chatIds: chatIds.filter((id) => selectedChats.has(id)) })
} else {
selectChatOnly(chatId)
setMenuSelection({ chatId, chatIds: [chatId] })
}
},
[chatIds]
)

const onContextMenu = useCallback(
(event: React.MouseEvent, chatId: string) => {
setSelectedChatId(chatId)
captureSelection(chatId)
hover.setLocked(true)
menu.preventDismiss()
menu.handleContextMenu(event)
},
[hover.setLocked, menu.preventDismiss, menu.handleContextMenu]
[captureSelection, hover.setLocked, menu.preventDismiss, menu.handleContextMenu]
)

const onMorePointerDown = useCallback(() => {
Expand All @@ -69,16 +98,16 @@ export function useOrganizationChatActions({

const onMoreClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>, chatId: string) => {
if (menu.isOpen && selectedChatId === chatId) {
if (menu.isOpen && menuSelection?.chatId === chatId) {
menu.closeMenu()
return
}
const rect = event.currentTarget.getBoundingClientRect()
setSelectedChatId(chatId)
captureSelection(chatId)
hover.setLocked(true)
menu.openMenuAt({ x: rect.right, y: rect.top })
},
[menu.isOpen, menu.closeMenu, menu.openMenuAt, hover.setLocked, selectedChatId]
[menu.isOpen, menu.closeMenu, menu.openMenuAt, hover.setLocked, menuSelection, captureSelection]
)

const chatId = selectedChat?.id
Expand All @@ -87,25 +116,54 @@ export function useOrganizationChatActions({
const chatPinned = selectedChat?.isPinned

const startDelete = useCallback(() => {
if (selectedChat) setChatToDelete(selectedChat)
}, [selectedChat])
setChatIdsToDelete(menuSelection?.chatIds ?? [])
}, [menuSelection])

const cancelDelete = useCallback(() => {
if (!isDeleting) setChatToDelete(null)
if (!isDeleting) setChatIdsToDelete([])
}, [isDeleting])

const confirmDelete = useCallback(() => {
if (!chatToDelete || isDeleting) return
deleteChat(chatToDelete.id, {
if (chatsToDelete.length === 0 || isDeleting) return
const redirectIfDeleted = (ids: string[]) => {
if (
ids.some((id) => window.location.pathname === organizationRoutes(organizationId).chat(id))
) {
router.push(organizationRoutes(organizationId).home)
}
}
const options = {
onSuccess: () => {
setChatToDelete(null)
if (window.location.pathname === chatToDelete.href) {
router.push(organizationRoutes(organizationId).home)
setChatIdsToDelete([])
useFolderStore.getState().clearChatSelection()
redirectIfDeleted(chatIdsToDelete)
},
onError: (error: Error) => {
if (error instanceof MothershipChatDeleteError) {
const deletedIds = new Set(error.deletedChatIds)
setChatIdsToDelete((ids) => ids.filter((id) => !deletedIds.has(id)))
redirectIfDeleted(error.deletedChatIds)
}
toast.error(error.message)
},
Comment thread
waleedlatif1 marked this conversation as resolved.
onError: (error) => toast.error(error.message),
})
}, [chatToDelete, deleteChat, isDeleting, organizationId, router])
}
if (chatsToDelete.length === 1) {
deleteChat.mutate(chatsToDelete[0].id, options)
} else {
deleteChats.mutate(
Comment thread
waleedlatif1 marked this conversation as resolved.
chatsToDelete.map((chat) => chat.id),
options
)
}
}, [
chatIdsToDelete,
chatsToDelete,
deleteChat.mutate,
deleteChats.mutate,
isDeleting,
organizationId,
router,
])

const startRename = useCallback(() => {
if (chatId && chatName !== undefined) rename.startRename({ id: chatId, name: chatName })
Expand Down Expand Up @@ -142,11 +200,15 @@ export function useOrganizationChatActions({
hover,
rename,
selectedChat,
selectedChats,
handleChatClick,
menuOpenChatId: menu.isOpen ? menuSelection?.chatId : null,
selectedCount: menuSelection?.chatIds.length ?? 0,
onContextMenu,
onMorePointerDown,
onMoreClick,
startRename,
chatToDelete,
chatsToDelete,
isDeleting,
startDelete,
cancelDelete,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ import {
SIDEBAR_SECTION_GAP_CLASS,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
import { useSidebarResize } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import { isSidebarBackgroundClick } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
import { useContextMenu } from '@/hooks/use-context-menu'
import { useFolderStore } from '@/stores/folders/store'
import { useSidebarStore } from '@/stores/sidebar/store'

const logger = createLogger('OrganizationSidebar')
Expand Down Expand Up @@ -144,6 +146,11 @@ export const OrganizationSidebar = memo(function OrganizationSidebar() {
}
}

const handleSidebarClick = (event: React.MouseEvent<HTMLElement>) => {
if (!isSidebarBackgroundClick(event)) return
useFolderStore.getState().clearChatSelection()
}

useRegisterGlobalCommands(() =>
createCommands([
{
Expand All @@ -161,6 +168,7 @@ export const OrganizationSidebar = memo(function OrganizationSidebar() {
className='group/rail sidebar-container relative h-full overflow-hidden bg-[var(--surface-1)] [&_.group.cursor-pointer]:duration-0'
data-collapsed={isCollapsed || undefined}
aria-label='Organization sidebar'
onClick={handleSidebarClick}
>
<div className='flex h-full flex-col'>
{/* The peek card already sits below the lane; reserving it again doubles the offset. */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface ChatNavigationLinkProps extends Omit<ComponentProps<typeof Link>, 'hre
chatId: string
href: string
isCurrentRoute?: boolean
onSelectChat?: (chatId: string, shiftKey: boolean) => void
}

export function ChatNavigationLink(props: ChatNavigationLinkProps) {
Expand All @@ -46,6 +47,7 @@ function IntentAwareChatNavigationLink({
isCurrentRoute = false,
onBlur,
onClick,
onSelectChat,
onFocus,
onMouseEnter,
onMouseLeave,
Expand Down Expand Up @@ -135,7 +137,16 @@ function IntentAwareChatNavigationLink({
}}
onTouchStart={onTouchStart}
onClick={(event) => {
const isSelectionClick =
!event.defaultPrevented && !event.metaKey && !event.ctrlKey && !event.altKey
if (isSelectionClick && event.shiftKey && onSelectChat) {
event.preventDefault()
onSelectChat(chatId, true)
}
onClick?.(event)
if (isSelectionClick && !event.defaultPrevented) {
onSelectChat?.(chatId, false)
}
if (
!event.defaultPrevented &&
!event.metaKey &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ type CollapsedSidebarMenuProps = {
interface CollapsedChatFlyoutItemProps {
chat: { id: string; href: string; name: string; isActive?: boolean; isUnread?: boolean }
isCurrentRoute: boolean
isSelected?: boolean
onSelectChat?: (chatId: string, shiftKey: boolean) => void
isMenuOpen?: boolean
isEditing?: boolean
editValue?: string
Expand Down Expand Up @@ -284,6 +286,8 @@ export function CollapsedSidebarMenu({
export function CollapsedChatFlyoutItem({
chat,
isCurrentRoute,
isSelected = false,
onSelectChat,
isMenuOpen = false,
isEditing = false,
editValue,
Expand Down Expand Up @@ -330,7 +334,7 @@ export function CollapsedChatFlyoutItem({
return (
<DropdownMenuItem
asChild
active={isCurrentRoute || isMenuOpen}
active={isCurrentRoute || isSelected || isMenuOpen}
actionOpen={isMenuOpen}
action={
showActions ? (
Expand All @@ -348,6 +352,7 @@ export function CollapsedChatFlyoutItem({
chatId={chat.id}
href={chat.href}
isCurrentRoute={isCurrentRoute}
onSelectChat={onSelectChat}
onContextMenu={
chat.id !== 'new' && onContextMenu ? (e) => onContextMenu(e, chat.id) : undefined
}
Expand Down
Loading
Loading