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
49 changes: 49 additions & 0 deletions apps/sim/app/api/organizations/[id]/search/history/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
clearSearchHistoryContract,
listSearchHistoryContract,
recordSearchHistoryContract,
} from '@/lib/api/contracts/knowledge/search-history'
import {
defineInternalJsonRoute,
internalOrchestrationErrorPolicy,
internalRateLimits,
internalSessionAuth,
} from '@/lib/api/server/routes'
import {
clearSearchHistory,
listSearchHistory,
recordSearchHistory,
searchHistoryOperations,
} from '@/lib/knowledge/application/search-history'

export const GET = defineInternalJsonRoute({
contract: listSearchHistoryContract,
auth: internalSessionAuth,
operation: searchHistoryOperations.list,
rateLimit: internalRateLimits.user({ bucketName: 'search-history-read' }),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params }) => ({ organizationId: params.id }),
useCase: listSearchHistory,
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
})
export const POST = defineInternalJsonRoute({
contract: recordSearchHistoryContract,
auth: internalSessionAuth,
operation: searchHistoryOperations.record,
rateLimit: internalRateLimits.user({ bucketName: 'search-history-write' }),
errorPolicy: internalOrchestrationErrorPolicy,
parseOptions: { maxBodyBytes: 24 * 1024 },
mapInput: ({ params, body }) => ({ organizationId: params.id, event: body }),
useCase: recordSearchHistory,
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
})
export const DELETE = defineInternalJsonRoute({
contract: clearSearchHistoryContract,
auth: internalSessionAuth,
operation: searchHistoryOperations.clear,
rateLimit: internalRateLimits.user({ bucketName: 'search-history-write' }),
errorPolicy: internalOrchestrationErrorPolicy,
mapInput: ({ params }) => ({ organizationId: params.id }),
useCase: clearSearchHistory,
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
})
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,7 @@ export function OrganizationHeader({
{logo}
</DropdownMenuItem>
</Tooltip.Trigger>
<Tooltip.Content>
{isUploadingLogo ? 'Uploading...' : 'Change logo'}
</Tooltip.Content>
<Tooltip.Content>{isUploadingLogo ? 'Uploading' : 'Change logo'}</Tooltip.Content>
</Tooltip.Root>
) : (
logo
Expand Down
114 changes: 114 additions & 0 deletions apps/sim/app/o/[organizationId]/components/search-landing-history.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use client'

import { type ReactNode, useState } from 'react'
import { Chip, cn, toast } from '@sim/emcn'
import { Clock } from '@sim/emcn/icons'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { SourceCard } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card'
import { useClearSearchHistory, useSearchHistory } from '@/hooks/queries/search-history'

interface SearchLandingHistoryProps {
organizationId: string
userId: string
onSearch: (query: string) => void
children: ReactNode
}

/** Private shortcuts under the composer; ordinary buttons retain native Tab/Enter navigation. */
export function SearchLandingHistory({
organizationId,
userId,
onSearch,
children,
}: SearchLandingHistoryProps) {
const history = useSearchHistory(organizationId)
const clear = useClearSearchHistory(organizationId, userId)
const [selection, setSelection] = useState<'sources' | 'queries' | null>(null)
const data = history.isError ? undefined : history.data
const sources = data?.sources.slice(0, 5) ?? []
const queries = data?.queries.slice(0, 5) ?? []
const selected = selection ?? (sources.length > 0 ? 'sources' : 'queries')
return (
<div className={cn('w-full min-w-0', inter.className)}>
{children}
{(sources.length > 0 || queries.length > 0) && (
<section aria-label='Recent activity' className='mt-6 px-2'>
<div className='mb-2 flex flex-wrap items-center justify-between gap-2'>
<div role='group' aria-label='History type' className='flex items-center gap-1'>
<Chip
active={selected === 'sources'}
aria-pressed={selected === 'sources'}
onClick={() => setSelection('sources')}
>
Recently viewed
</Chip>
<Chip
active={selected === 'queries'}
aria-pressed={selected === 'queries'}
onClick={() => setSelection('queries')}
>
Recent searches
</Chip>
</div>
<Chip
disabled={clear.isPending}
onClick={() =>
clear.mutate(undefined, { onError: (error) => toast.error(error.message) })
}
>
Clear history
</Chip>
</div>
<div className='grid grid-cols-1'>
<div
className={cn(
'col-start-1 row-start-1 min-w-0',
selected !== 'sources' && 'invisible'
)}
inert={selected !== 'sources'}
aria-hidden={selected !== 'sources'}
>
{sources.length > 0 ? (
sources.map((source) => <SourceCard key={source.url} source={source} dense />)
) : (
<p className='px-2 py-2 text-[var(--text-tertiary)] text-small'>
Sources you open will appear here.
</p>
)}
</div>
<div
className={cn(
'col-start-1 row-start-1 min-w-0',
selected !== 'queries' && 'invisible'
)}
inert={selected !== 'queries'}
aria-hidden={selected !== 'queries'}
>
{queries.length > 0 ? (
queries.map(({ query }) => (
<div key={query} className='py-1'>
<Chip fullWidth leftIcon={Clock} onClick={() => onSearch(query)}>
{query}
</Chip>
</div>
))
) : (
<p className='px-2 py-2 text-[var(--text-tertiary)] text-small'>
Your recent searches will appear here.
</p>
)}
</div>
</div>
</section>
)}
{history.isError && (
<div className='mt-6 flex items-center gap-2 px-4 text-[var(--text-tertiary)] text-small'>
Recent activity couldn’t load.
<Chip onClick={() => void history.refetch()} disabled={history.isFetching}>
Try again
</Chip>
</div>
)}
</div>
)
}
27 changes: 24 additions & 3 deletions apps/sim/app/o/[organizationId]/home/organization-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import {
getMothershipAttachmentUrl,
} from '@/lib/mothership/chat/attachment-preview'
import { createSearchResource } from '@/lib/mothership/resources/search'
import { SearchLandingHistory } from '@/app/o/[organizationId]/components/search-landing-history'
import { Composer } from '@/app/o/[organizationId]/home/components/composer'
import { GetStarted } from '@/app/o/[organizationId]/home/components/get-started'
import { organizationHomeParsers } from '@/app/o/[organizationId]/home/search-params'
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
import { organizationSearchUrlKeys } from '@/app/o/[organizationId]/search/search-params'
import { ChatResourcePanel } from '@/app/workspace/[workspaceId]/home/components/chat-resource-panel'
import { useSearchHistoryActions } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-history-context'
import { SearchIntegrationConnection } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/search-integration-connection'
import { MothershipChat } from '@/app/workspace/[workspaceId]/home/components/mothership-chat'
import { SuggestedActions } from '@/app/workspace/[workspaceId]/home/components/suggested-actions'
Expand Down Expand Up @@ -223,6 +225,7 @@ function OrganizationHomeContent({
useEffect(() => {
if (chat.error) toast.error(chat.error)
}, [chat.error])
const { recordQuery } = useSearchHistoryActions()
const { sendMessage } = chat
const { mutate: markRead } = useMarkMothershipChatRead({ organizationId: organization.id })
const firstName = userName?.split(' ')[0] ?? ''
Expand Down Expand Up @@ -263,6 +266,7 @@ function OrganizationHomeContent({
assistantSearch?: WorkspaceSearchFilters
) => {
if (requestMode !== 'assistant' && !canBuild) return
if (requestMode === 'assistant') recordQuery(message)
setSelectedMode(requestMode)
if (requestMode !== 'assistant') panel.prepareResourceViewForAgentTurn()
void sendMessage(message, fileAttachments, contexts, {
Expand Down Expand Up @@ -334,6 +338,14 @@ function OrganizationHomeContent({
<div className='flex h-full min-h-0 min-w-[min(480px,100%)] flex-1 flex-col bg-[var(--bg)]'>
{hasChat ? (
<MothershipChat
onViewSources={(messageId, requestId) =>
addResource({
type: 'sources',
id: 'cited-sources',
title: 'Sources',
sources: { messageId, ...(requestId ? { requestId } : {}) },
})
}
SearchConnectionComponent={SearchIntegrationConnection}
messages={chat.messages}
isSending={chat.isSending}
Expand Down Expand Up @@ -396,9 +408,18 @@ function OrganizationHomeContent({
: `What should we get done${firstName ? `, ${firstName}` : ''}?`}
</h1>
<div className='relative w-full max-w-chat'>
{composer}
{/* Anchored out of flow so expanding/collapsing never shifts the centered input */}
<div className='absolute inset-x-0 top-full'>
{requestMode === 'assistant' && searchAccess.memberScoped && userId ? (
<SearchLandingHistory
organizationId={organization.id}
userId={userId}
onSearch={(query) => submit(query)}
>
{composer}
</SearchLandingHistory>
) : (
composer
)}
<div className={requestMode === 'agent' ? 'absolute inset-x-0 top-full' : 'mt-4'}>
{requestMode === 'agent' ? (
<SuggestedActions
organizationId={organization.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function DisconnectAccountMenu({
text={`Disconnect ${selected ? accountLabel(selected) : integrationName} from all ${integrationName} connections in this organization. Workflows using this account will also lose access. You can reconnect later.`}
confirm={{
label: 'Disconnect',
pendingLabel: 'Disconnecting…',
pendingLabel: 'Disconnecting',
pending: disconnect.isPending,
disabled: disconnect.isPending,
onClick: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function GitHubMemberIntegration({
const description = account
? `${accounts.map((entry) => entry.displayName).join(', ')} · ${account.status === 'needs_reauth' ? 'Reconnect required' : 'Connected'}`
: loading
? 'Loading connection…'
? 'Loading connection'
: failed
? 'Could not load connection'
: option
Expand All @@ -68,7 +68,7 @@ export function GitHubMemberIntegration({
/>
{failed ? (
<Chip disabled={inventory.isFetching} onClick={() => void inventory.refetch()}>
{inventory.isFetching ? 'Retrying…' : 'Retry'}
{inventory.isFetching ? 'Retrying' : 'Retry'}
</Chip>
) : account?.status === 'needs_reauth' && option?.id === account.optionId ? (
<Chip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ describe('grouped member integrations', () => {
expect(mocks.nextPage).toHaveBeenCalledOnce()
queryOverrides = { hasNextPage: true, isFetchingNextPage: true, isFetching: true }
await render()
expect(buttons('Checking…')[0]).toBeDisabled()
expect(buttons('Checking')[0]).toBeDisabled()
rows = [
...rows,
{ ...memberSource, connectorId: 'older-source', viewerMembership: membership },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function LiveMemberIntegrations({ organizationId, search }: LiveMemberInt
/>
)
if (!inventory.data || !policies.data || !secrets.data)
return <SettingsEmptyState variant='inline'>Loading your connections…</SettingsEmptyState>
return <SettingsEmptyState variant='inline'>Loading your connections</SettingsEmptyState>
const data = inventory.data
const approvals = new Map(policies.data.map((policy) => [policy.connectorType, policy]))
const group = data.credentialGroup
Expand Down Expand Up @@ -210,7 +210,7 @@ export function LiveMemberIntegrations({ organizationId, search }: LiveMemberInt
connect.variables.optionId === option?.id) ||
('mcpServerId' in connect.variables &&
connect.variables.mcpServerId === server?.id))
? 'Connecting…'
? 'Connecting'
: accounts.length
? 'Add account'
: 'Connect'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function MemberIntegrationRow({
function description() {
if (!configured) return waiting ? 'Finish connecting in the other tab' : 'Not connected'
if (hasLoadError) return 'Could not load connection'
if (sources.isPending) return 'Loading connection…'
if (sources.isPending) return 'Loading connection'
if (target) {
if (waiting) return 'Finish connecting in the other tab'
if (target.viewerMembership === 'needs_reauth') return 'Reconnect your account'
Expand Down Expand Up @@ -183,7 +183,7 @@ export function MemberIntegrationRow({
)}
{hasLoadError && (
<Chip disabled={sources.isFetching} onClick={() => void sources.refetch()}>
{sources.isFetching ? 'Retrying…' : 'Retry'}
{sources.isFetching ? 'Retrying' : 'Retry'}
</Chip>
)}
{canCheckConnections && (
Expand All @@ -193,7 +193,7 @@ export function MemberIntegrationRow({
onClick={() => void sources.fetchNextPage({ cancelRefetch: false })}
>
{sources.isFetchingNextPage
? 'Checking…'
? 'Checking'
: sources.isFetchNextPageError
? 'Retry'
: 'Check connections'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export function MemberIntegrationsList({
</>
) : overview.isPending || integrations.isPending ? (
<>
<SettingsEmptyState variant='inline'>Loading integrations…</SettingsEmptyState>
<SettingsEmptyState variant='inline'>Loading integrations</SettingsEmptyState>
{githubRow}
</>
) : (
Expand Down Expand Up @@ -178,7 +178,7 @@ export function MemberIntegrationsList({
<SettingsEmptyState variant='inline'>
{!availability.isIntegrationAvailabilityReady ||
(approved.has('slack') && 'slack'.includes(query) && slackInventory.isPending)
? 'Loading integrations…'
? 'Loading integrations'
: search
? 'No matching integrations.'
: 'No integrations are available to connect.'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ export function SlackSearchActions({ organizationId, token, userId }: SlackSearc
disabled={retry.isPending}
onClick={() => retry.mutate({ token })}
>
{retry.isPending ? 'Queuing…' : 'Retry question in Slack'}
{retry.isPending ? 'Queuing' : 'Retry question in Slack'}
</Chip>
) : (
<Chip disabled={status.isFetching} onClick={() => void status.refetch()}>
{status.isFetching ? 'Checking…' : 'Check connection'}
{status.isFetching ? 'Checking' : 'Check connection'}
</Chip>
))}
<ChipLink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { act } from 'react'
import { authClientMock, authClientMockFns } from '@sim/testing/mocks/auth-client.mock'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

Expand Down Expand Up @@ -39,8 +40,10 @@ function ScimReader() {

let host: HTMLDivElement
let root: Root
let client: QueryClient

beforeEach(() => {
client = new QueryClient()
resetDeploymentShape()
host = document.createElement('div')
document.body.appendChild(host)
Expand All @@ -49,6 +52,7 @@ beforeEach(() => {

afterEach(() => {
act(() => root.unmount())
client.clear()
host.remove()
})

Expand All @@ -69,9 +73,11 @@ describe('OrganizationProvider', () => {

act(() =>
root.render(
<OrganizationProvider context={context}>
<ScimReader />
</OrganizationProvider>
<QueryClientProvider client={client}>
<OrganizationProvider context={context}>
<ScimReader />
</OrganizationProvider>
</QueryClientProvider>
)
)

Expand Down
Loading
Loading