Skip to content

Commit 95047af

Browse files
committed
fix(organizations): align sidebar loading and chat actions
1 parent bfa61f3 commit 95047af

14 files changed

Lines changed: 808 additions & 207 deletions

File tree

apps/sim/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section.test.tsx

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
88
import type { OrganizationChat } from '@/app/o/[organizationId]/components/organization-sidebar/hooks'
99

1010
const hoverState = vi.hoisted(() => ({ isOpen: false }))
11+
const mockRequestJson = vi.hoisted(() => vi.fn())
12+
13+
vi.mock('@/lib/api/client/request', () => ({ requestJson: mockRequestJson }))
1114

1215
vi.mock('next/link', () => ({
1316
default: ({
@@ -25,7 +28,7 @@ vi.mock('next/link', () => ({
2528
</a>
2629
),
2730
}))
28-
vi.mock('@/app/workspace/[workspaceId]/w/components/sidebar/hooks', () => ({
31+
vi.mock('@/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu', () => ({
2932
useHoverMenu: () => ({
3033
isOpen: hoverState.isOpen,
3134
open: vi.fn(),
@@ -37,6 +40,7 @@ vi.mock('@/app/workspace/[workspaceId]/w/components/sidebar/hooks', () => ({
3740
}))
3841

3942
import { ChatsSection } from '@/app/o/[organizationId]/components/organization-sidebar/components/chats-section/chats-section'
43+
import { mothershipChatKeys } from '@/hooks/queries/mothership-chats'
4044

4145
const CHATS: OrganizationChat[] = Array.from({ length: 8 }, (_, index) => ({
4246
id: `chat-${index + 1}`,
@@ -59,6 +63,8 @@ beforeEach(() => {
5963
disconnect() {}
6064
}
6165
)
66+
vi.clearAllMocks()
67+
mockRequestJson.mockResolvedValue({ success: true })
6268
hoverState.isOpen = false
6369
queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
6470
prefetchQuery = vi.spyOn(queryClient, 'prefetchQuery').mockResolvedValue()
@@ -83,9 +89,7 @@ async function render(props: Partial<Parameters<typeof ChatsSection>[0]> = {}) {
8389
isLoading={false}
8490
isCollapsed={false}
8591
pathname={null}
86-
menuOpenHref={null}
87-
onContextMenu={() => {}}
88-
onMoreClick={() => {}}
92+
organizationId='org-1'
8993
{...props}
9094
/>
9195
</QueryClientProvider>
@@ -94,11 +98,19 @@ async function render(props: Partial<Parameters<typeof ChatsSection>[0]> = {}) {
9498
}
9599

96100
describe('ChatsSection', () => {
97-
it('lists every chat with no paging control', async () => {
101+
it('shows five chats with the workspace-style See more and See less controls', async () => {
98102
await render()
99-
103+
expect(container.querySelectorAll('a[href^="/o/org-1/chat/"]')).toHaveLength(5)
104+
const more = Array.from(container.querySelectorAll('button')).find(
105+
(button) => button.textContent === 'See more'
106+
)!
107+
await act(async () => more.click())
100108
expect(container.querySelectorAll('a[href^="/o/org-1/chat/"]')).toHaveLength(8)
101-
expect(container.textContent).not.toContain('See more')
109+
const less = Array.from(container.querySelectorAll('button')).find(
110+
(button) => button.textContent === 'See less'
111+
)!
112+
await act(async () => less.click())
113+
expect(container.querySelectorAll('a[href^="/o/org-1/chat/"]')).toHaveLength(5)
102114
})
103115

104116
it('marks the chat on the current route active', async () => {
@@ -110,17 +122,108 @@ describe('ChatsSection', () => {
110122
expect(other?.className).not.toContain('surface-active')
111123
})
112124

113-
it('reports the row href when its options button is pressed', async () => {
114-
const onMoreClick = vi.fn()
115-
await render({ onMoreClick })
125+
it.each([false, true])('renames via the options menu with collapsed=%s', async (isCollapsed) => {
126+
hoverState.isOpen = isCollapsed
127+
await render({ isCollapsed })
128+
const button =
129+
document.body.querySelector<HTMLButtonElement>(
130+
'a[href="/o/org-1/chat/chat-2"] button[aria-label="Chat options"]'
131+
) ?? document.body.querySelector<HTMLButtonElement>('[aria-label="Chat options"]')!
132+
await act(async () => button.click())
133+
const rename = Array.from(
134+
document.body.querySelectorAll<HTMLElement>('[role="menuitem"]')
135+
).find((item) => item.textContent === 'Rename')!
136+
expect(rename).toBeDefined()
137+
await act(async () => rename.click())
138+
const input = document.body.querySelector<HTMLInputElement>('input[aria-label^="Rename chat"]')!
139+
expect(input).not.toBeNull()
140+
expect(input.value).toMatch(/^Chat /)
141+
await act(async () => {
142+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(
143+
input,
144+
'Planning'
145+
)
146+
input.dispatchEvent(new Event('input', { bubbles: true }))
147+
})
148+
await act(async () =>
149+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
150+
)
151+
expect(mockRequestJson).toHaveBeenCalledWith(
152+
expect.objectContaining({ method: 'PATCH' }),
153+
expect.objectContaining({ body: { title: 'Planning' } })
154+
)
155+
expect(document.body.querySelector('input[aria-label^="Rename chat"]')).toBeNull()
156+
})
157+
158+
it('rolls back only the organization list when rename fails', async () => {
159+
const pending = Promise.withResolvers<{ success: boolean }>()
160+
mockRequestJson.mockReturnValueOnce(pending.promise)
161+
const key = mothershipChatKeys.organizationList('org-1')
162+
queryClient.setQueryData(key, [{ id: 'chat-1', name: 'Chat 1' }])
163+
const workspaceKey = mothershipChatKeys.list('workspace-1')
164+
queryClient.setQueryData(workspaceKey, [{ id: 'workspace-chat', name: 'Workspace chat' }])
165+
await render()
166+
await act(async () =>
167+
container.querySelector<HTMLButtonElement>('[aria-label="Chat options"]')!.click()
168+
)
169+
const action = Array.from(
170+
document.body.querySelectorAll<HTMLElement>('[role="menuitem"]')
171+
).find((item) => item.textContent === 'Rename')!
172+
await act(async () => action.click())
173+
const input = document.body.querySelector<HTMLInputElement>('input[aria-label^="Rename chat"]')!
174+
await act(async () => {
175+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')!.set!.call(
176+
input,
177+
'Pending title'
178+
)
179+
input.dispatchEvent(new Event('input', { bubbles: true }))
180+
})
181+
await act(async () =>
182+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
183+
)
184+
expect(queryClient.getQueryData(key)).toEqual([{ id: 'chat-1', name: 'Pending title' }])
185+
await act(async () => pending.reject(new Error('Rename rejected')))
186+
expect(queryClient.getQueryData(key)).toEqual([{ id: 'chat-1', name: 'Chat 1' }])
187+
expect(queryClient.getQueryData(workspaceKey)).toEqual([
188+
{ id: 'workspace-chat', name: 'Workspace chat' },
189+
])
190+
expect(input.value).toBe('Chat 1')
191+
expect(input.disabled).toBe(false)
192+
})
116193

117-
const button = container.querySelector<HTMLButtonElement>(
118-
'a[href="/o/org-1/chat/chat-2"] button[aria-label="Chat options"]'
194+
it('cancels rename on Escape without a mutation', async () => {
195+
await render()
196+
await act(async () =>
197+
container.querySelector<HTMLButtonElement>('[aria-label="Chat options"]')!.click()
198+
)
199+
const rename = Array.from(
200+
document.body.querySelectorAll<HTMLElement>('[role="menuitem"]')
201+
).find((item) => item.textContent === 'Rename')!
202+
await act(async () => rename.click())
203+
const input = document.body.querySelector<HTMLInputElement>('input[aria-label^="Rename chat"]')!
204+
await act(async () =>
205+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
119206
)
120-
await act(async () => button?.click())
207+
expect(mockRequestJson).not.toHaveBeenCalled()
208+
expect(document.body.querySelector('input[aria-label^="Rename chat"]')).toBeNull()
209+
})
121210

122-
expect(onMoreClick).toHaveBeenCalledWith(expect.anything(), '/o/org-1/chat/chat-2')
123-
expect(prefetchQuery).not.toHaveBeenCalled()
211+
it.each([
212+
['Pin', { pinned: true }],
213+
['Mark as unread', { isUnread: true }],
214+
])('offers %s for organization chats', async (label, body) => {
215+
await render()
216+
await act(async () =>
217+
container.querySelector<HTMLButtonElement>('[aria-label="Chat options"]')!.click()
218+
)
219+
const action = Array.from(
220+
document.body.querySelectorAll<HTMLElement>('[role="menuitem"]')
221+
).find((item) => item.textContent === label)!
222+
await act(async () => action.click())
223+
expect(mockRequestJson).toHaveBeenCalledWith(expect.objectContaining({ method: 'PATCH' }), {
224+
params: { chatId: 'chat-1' },
225+
body,
226+
})
124227
})
125228

126229
it.each([false, true])(

0 commit comments

Comments
 (0)