Skip to content

Commit 25e6091

Browse files
authored
fix(search): disambiguate tables and knowledge bases by folder (#6192)
* fix(search): disambiguate tables and knowledge bases by folder The Cmd-K search modal listed tables and knowledge bases without the folder breadcrumb workflows and files already showed, and the table, knowledge base, and search-and-replace pickers in the workflow editor rendered bare names -- so two resources sharing a name in different folders were indistinguishable. Extracts the disambiguation the workflow selector already did into shared collectDuplicateNames + disambiguateLabelByFolder, and shares the search row's folder breadcrumb and its memo comparator, which were duplicated between the workflow and file rows. Also routes folder text through filterAndCap's secondary-rank parameter rather than concatenating it into the name, so an exact name match can no longer be outranked by a folder that happens to fuzzy-match. * fix(zoho-desk): use the real Zoho Desk mark on a white tile The icon was a generic headset placeholder drawn in currentColor, so it never resembled Zoho at all. Replaces it with the mark from Zoho's official logo -- wordmark stripped, viewBox set to the mark's own bounding box so it centers -- and moves the tile to white, matching the other brand-mark integrations.
1 parent f113b0b commit 25e6091

17 files changed

Lines changed: 334 additions & 136 deletions

File tree

apps/docs/components/icons.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8932,14 +8932,14 @@ export function ZohoDeskIcon(props: SVGProps<SVGSVGElement>) {
89328932
return (
89338933
<svg
89348934
{...props}
8935-
viewBox='0 0 24 24'
8935+
viewBox='-24 -6 308 308'
89368936
fill='none'
89378937
xmlns='http://www.w3.org/2000/svg'
89388938
aria-hidden='true'
89398939
>
89408940
<path
8941-
d='M12 2.75c-4.28 0-7.75 3.47-7.75 7.75v3.1A2.6 2.6 0 0 0 3 16.35v1.3A2.6 2.6 0 0 0 5.6 20.25h1.15a.9.9 0 0 0 .9-.9v-4.9a.9.9 0 0 0-.9-.9H6.05v-2.15a5.95 5.95 0 0 1 11.9 0v2.15h-.7a.9.9 0 0 0-.9.9v4.9c0 .17.05.33.13.47-.5.6-1.24.98-2.08.98h-1.02a1.4 1.4 0 0 0-1.31-.9h-1a1.4 1.4 0 0 0 0 2.8h1a1.4 1.4 0 0 0 1.31-.9h1.02c2.06 0 3.74-1.63 3.83-3.67a2.6 2.6 0 0 0 1.44-2.33v-1.3a2.6 2.6 0 0 0-1.25-2.22v-3.1c0-4.28-3.47-7.75-7.75-7.75Z'
8942-
fill='currentColor'
8941+
d='M22.3,293c-1.6,0-3.3-0.5-4.7-1.4c-2.8-1.8-4.2-5.1-3.7-8.4l24.3-149.3c1.3-8,5.4-15.2,11.5-20.4 c6.1-5.2,14-8.1,22-8.1h100.1l5.6-34.3H168c-4.2,0-8.1-1.8-10.8-5c-2.7-3.2-3.9-7.3-3.2-11.4l6.4-39.7c1.1-6.9,7-11.9,14-11.9h37.3 c10,0,19.4,4.4,25.9,12s9.3,17.6,7.7,27.5l-15,92.4c-0.7,4-3,7.6-6.5,9.8L140,196.5c-5.1,3.1-11.6,2.8-16.3-1l-39-31.1 c-3.7-2.9-4.3-8.3-1.3-11.9c2.9-3.7,8.3-4.3,11.9-1.3l37.5,29.9l81-50.2l14.8-91.1c0.8-4.9-0.6-9.9-3.8-13.7c-3.2-3.8-7.9-6-12.9-6 h-34.9l-5.5,34h9.3c4.2,0,8.1,1.8,10.8,5c2.7,3.2,3.9,7.3,3.2,11.4l-6.5,39.9c-1.1,6.9-7,11.9-14,11.9H71.7 c-8.4,0-15.4,6-16.8,14.3L33.6,267.5L63.5,249c2.2-1.4,4.8-2.1,7.5-2.1h109.5c8.4,0,15.4-6,16.8-14.3l6.8-41.9 c0.8-4.6,5.1-7.8,9.7-7c4.6,0.8,7.8,5.1,7,9.7l-6.8,41.9c-1.3,8-5.4,15.2-11.5,20.4c-6.1,5.2-14,8.1-22,8.1H71.7l-45,27.9 C25.4,292.6,23.8,293,22.3,293z'
8942+
fill='#089949'
89438943
/>
89448944
</svg>
89458945
)

apps/docs/content/docs/en/integrations/zoho_desk.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
77

88
<BlockInfoCard
99
type="zoho_desk"
10-
color="#E42527"
10+
color="#FFFFFF"
1111
/>
1212

1313
{/* MANUAL-CONTENT-START:intro */}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
1313
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
1414
import type { SubBlockConfig } from '@/blocks/types'
1515
import { useKnowledgeBasesList } from '@/hooks/kb/use-knowledge'
16+
import { useFolderMap } from '@/hooks/queries/folders'
1617
import { fetchKnowledgeBase } from '@/hooks/queries/kb/knowledge'
18+
import { collectDuplicateNames, disambiguateLabelByFolder } from '@/hooks/queries/utils/folder-tree'
1719
import { knowledgeKeys } from '@/hooks/queries/utils/knowledge-keys'
1820

1921
interface KnowledgeBaseSelectorProps {
@@ -43,6 +45,8 @@ export function KnowledgeBaseSelector({
4345
error,
4446
} = useKnowledgeBasesList(workspaceId)
4547

48+
const { data: knowledgeBaseFolders = {} } = useFolderMap(workspaceId, 'knowledge_base')
49+
4650
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
4751

4852
const value = isPreview ? previewValue : storeValue
@@ -90,13 +94,32 @@ export function KnowledgeBaseSelector({
9094
return Array.from(merged.values())
9195
}, [knowledgeBases, selectedKnowledgeBaseQueries])
9296

93-
const options = useMemo<ComboboxOption[]>(() => {
94-
return combinedKnowledgeBases.map((kb) => ({
95-
label: kb.name,
96-
value: kb.id,
97-
icon: PackageSearchIcon,
98-
}))
99-
}, [combinedKnowledgeBases])
97+
/**
98+
* Display names, with the folder path appended when two knowledge bases share
99+
* a name — otherwise the dropdown rows and the selected chips are
100+
* indistinguishable from one another. Built in the same pass as the options so
101+
* the chips and the dropdown can never disagree.
102+
*/
103+
const { options, labelById } = useMemo(() => {
104+
const duplicateNames = collectDuplicateNames(combinedKnowledgeBases.map((kb) => kb.name))
105+
const labelById = new Map<string, string>()
106+
const options: ComboboxOption[] = combinedKnowledgeBases.map((kb) => {
107+
const label = disambiguateLabelByFolder(
108+
kb.name,
109+
kb.folderId,
110+
knowledgeBaseFolders,
111+
duplicateNames
112+
)
113+
labelById.set(kb.id, label)
114+
return { label, value: kb.id, icon: PackageSearchIcon }
115+
})
116+
return { options, labelById }
117+
}, [combinedKnowledgeBases, knowledgeBaseFolders])
118+
119+
const labelOf = useCallback(
120+
(kb: KnowledgeBaseData) => labelById.get(kb.id) ?? kb.name,
121+
[labelById]
122+
)
100123

101124
/**
102125
* Compute selected knowledge bases for tag display
@@ -172,7 +195,7 @@ export function KnowledgeBaseSelector({
172195
blockId,
173196
subBlockId: subBlock.id,
174197
valuePath: [index],
175-
label: kb.name,
198+
label: labelOf(kb),
176199
})
177200
return (
178201
<div
@@ -181,14 +204,14 @@ export function KnowledgeBaseSelector({
181204
>
182205
<PackageSearchIcon className='mr-1 size-3 text-[var(--brand-knowledge)]' />
183206
<span className='font-medium text-[var(--brand-knowledge)]'>
184-
{formatDisplayText(kb.name, { workflowSearchHighlight })}
207+
{formatDisplayText(labelOf(kb), { workflowSearchHighlight })}
185208
</span>
186209
{!disabled && !isPreview && (
187210
<button
188211
type='button'
189212
onClick={() => handleRemoveKnowledgeBase(kb.id)}
190213
className='ml-1 text-[color-mix(in_srgb,var(--brand-knowledge)_60%,transparent)] hover-hover:text-[var(--brand-knowledge)]'
191-
aria-label={`Remove ${kb.name}`}
214+
aria-label={`Remove ${labelOf(kb)}`}
192215
>
193216
<X className='size-3' />
194217
</button>
@@ -220,11 +243,13 @@ export function KnowledgeBaseSelector({
220243
blockId,
221244
subBlockId: subBlock.id,
222245
valuePath: [],
223-
label: selectedKnowledgeBases[0].name,
246+
label: labelOf(selectedKnowledgeBases[0]),
224247
})
225248
return workflowSearchHighlight ? (
226249
<span className='truncate text-[var(--text-primary)]'>
227-
{formatDisplayText(selectedKnowledgeBases[0].name, { workflowSearchHighlight })}
250+
{formatDisplayText(labelOf(selectedKnowledgeBases[0]), {
251+
workflowSearchHighlight,
252+
})}
228253
</span>
229254
) : undefined
230255
})()

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table-selector/table-selector.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w
88
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
99
import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/providers/active-search-target-provider'
1010
import type { SubBlockConfig } from '@/blocks/types'
11+
import { useFolderMap } from '@/hooks/queries/folders'
1112
import { useTablesList } from '@/hooks/queries/tables'
13+
import { collectDuplicateNames, disambiguateLabelByFolder } from '@/hooks/queries/utils/folder-tree'
1214

1315
interface TableSelectorProps {
1416
blockId: string
@@ -45,15 +47,33 @@ export function TableSelector({
4547
error,
4648
} = useTablesList(isPreview || disabled ? undefined : workspaceId)
4749

50+
const { data: tableFolders = {} } = useFolderMap(
51+
isPreview || disabled ? undefined : workspaceId,
52+
'table'
53+
)
54+
4855
const value = isPreview ? previewValue : storeValue
4956
const tableId = typeof value === 'string' ? value : null
5057

58+
/**
59+
* Two tables can share a name in different folders, so a colliding name is
60+
* suffixed with its folder path. Table names are lowercased for display (the
61+
* pre-existing styling here), and collisions are detected on that same
62+
* lowercased form so `Leads` and `leads` — identical once displayed — are
63+
* disambiguated too. The folder path keeps its authored casing.
64+
*/
5165
const options = useMemo<ComboboxOption[]>(() => {
66+
const duplicateNames = collectDuplicateNames(tables.map((table) => table.name.toLowerCase()))
5267
return tables.map((table) => ({
53-
label: table.name.toLowerCase(),
68+
label: disambiguateLabelByFolder(
69+
table.name.toLowerCase(),
70+
table.folderId,
71+
tableFolders,
72+
duplicateNames
73+
),
5474
value: table.id,
5575
}))
56-
}, [tables])
76+
}, [tables, tableFolders])
5777

5878
const handleChange = useCallback(
5979
(selectedValue: string) => {

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,35 @@ export const MemoizedActionItem = memo(
7878
prev.shortcut === next.shortcut
7979
)
8080

81+
/**
82+
* Right-aligned folder breadcrumb. All but the last segment collapse first so a
83+
* deep path degrades to the immediate parent rather than truncating the whole
84+
* trail. Renders nothing at the workspace root.
85+
*/
86+
function FolderPathSuffix({ folderPath }: { folderPath?: string[] }) {
87+
if (!folderPath || folderPath.length === 0) return null
88+
return (
89+
<span className='ml-auto flex min-w-0 pl-2 text-[var(--text-subtle)] text-small'>
90+
{folderPath.length > 1 && (
91+
<>
92+
<span className='min-w-0 truncate [flex-shrink:9999]'>
93+
{folderPath.slice(0, -1).join(' / ')}
94+
</span>
95+
<span className='flex-shrink-0 whitespace-pre'> / </span>
96+
</>
97+
)}
98+
<span className='min-w-0 truncate'>{folderPath[folderPath.length - 1]}</span>
99+
</span>
100+
)
101+
}
102+
103+
/** Element-wise compare so a rebuilt-but-identical path array skips the re-render. */
104+
function sameFolderPath(a?: string[], b?: string[]): boolean {
105+
if (a === b) return true
106+
if (a?.length !== b?.length) return false
107+
return (a ?? []).every((segment, i) => segment === b?.[i])
108+
}
109+
81110
export const MemoizedWorkflowItem = memo(
82111
function WorkflowItem({
83112
value,
@@ -101,29 +130,15 @@ export const MemoizedWorkflowItem = memo(
101130
<span className='truncate'>{name}</span>
102131
{isCurrent && <span className='flex-shrink-0 whitespace-pre'> (current)</span>}
103132
</span>
104-
{folderPath && folderPath.length > 0 && (
105-
<span className='ml-auto flex min-w-0 pl-2 text-[var(--text-subtle)] text-small'>
106-
{folderPath.length > 1 && (
107-
<>
108-
<span className='min-w-0 truncate [flex-shrink:9999]'>
109-
{folderPath.slice(0, -1).join(' / ')}
110-
</span>
111-
<span className='flex-shrink-0 whitespace-pre'> / </span>
112-
</>
113-
)}
114-
<span className='min-w-0 truncate'>{folderPath[folderPath.length - 1]}</span>
115-
</span>
116-
)}
133+
<FolderPathSuffix folderPath={folderPath} />
117134
</Command.Item>
118135
)
119136
},
120137
(prev, next) =>
121138
prev.value === next.value &&
122139
prev.name === next.name &&
123140
prev.isCurrent === next.isCurrent &&
124-
(prev.folderPath === next.folderPath ||
125-
(prev.folderPath?.length === next.folderPath?.length &&
126-
(prev.folderPath ?? []).every((segment, i) => segment === next.folderPath?.[i])))
141+
sameFolderPath(prev.folderPath, next.folderPath)
127142
)
128143

129144
export const MemoizedFileItem = memo(
@@ -143,31 +158,17 @@ export const MemoizedFileItem = memo(
143158
<div className='relative flex size-[16px] flex-shrink-0 items-center justify-center'>
144159
<File className='size-[14px] text-[var(--text-icon)]' />
145160
</div>
146-
<span className='flex min-w-0 max-w-[75%] flex-shrink-0 font-base text-[var(--text-body)]'>
161+
<span className='flex min-w-0 max-w-[75%] flex-shrink-0 text-[var(--text-body)]'>
147162
<span className='truncate'>{name}</span>
148163
</span>
149-
{folderPath && folderPath.length > 0 && (
150-
<span className='ml-auto flex min-w-0 pl-2 font-base text-[var(--text-subtle)] text-small'>
151-
{folderPath.length > 1 && (
152-
<>
153-
<span className='min-w-0 truncate [flex-shrink:9999]'>
154-
{folderPath.slice(0, -1).join(' / ')}
155-
</span>
156-
<span className='flex-shrink-0 whitespace-pre'> / </span>
157-
</>
158-
)}
159-
<span className='min-w-0 truncate'>{folderPath[folderPath.length - 1]}</span>
160-
</span>
161-
)}
164+
<FolderPathSuffix folderPath={folderPath} />
162165
</Command.Item>
163166
)
164167
},
165168
(prev, next) =>
166169
prev.value === next.value &&
167170
prev.name === next.name &&
168-
(prev.folderPath === next.folderPath ||
169-
(prev.folderPath?.length === next.folderPath?.length &&
170-
(prev.folderPath ?? []).every((segment, i) => segment === next.folderPath?.[i])))
171+
sameFolderPath(prev.folderPath, next.folderPath)
171172
)
172173

173174
export const MemoizedTaskItem = memo(
@@ -253,18 +254,27 @@ export const MemoizedIconItem = memo(
253254
onSelect,
254255
name,
255256
icon: Icon,
257+
folderPath,
256258
}: {
257259
value: string
258260
onSelect: () => void
259261
name: string
260262
icon: ComponentType<{ className?: string }>
263+
folderPath?: string[]
261264
}) {
262265
return (
263266
<Command.Item value={value} onSelect={onSelect} className={COMMAND_ITEM_CLASSNAME}>
264267
<Icon className='size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
265-
<span className='truncate text-[var(--text-body)]'>{name}</span>
268+
<span className='flex min-w-0 max-w-[75%] flex-shrink-0 text-[var(--text-body)]'>
269+
<span className='truncate'>{name}</span>
270+
</span>
271+
<FolderPathSuffix folderPath={folderPath} />
266272
</Command.Item>
267273
)
268274
},
269-
(prev, next) => prev.value === next.value && prev.name === next.name && prev.icon === next.icon
275+
(prev, next) =>
276+
prev.value === next.value &&
277+
prev.name === next.name &&
278+
prev.icon === next.icon &&
279+
sameFolderPath(prev.folderPath, next.folderPath)
270280
)

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/search-groups/search-groups.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import type {
1818
ActionItem,
1919
FileItem,
20+
FolderedItem,
2021
IntegrationSearchItem,
2122
PageItem,
2223
TaskItem,
@@ -343,19 +344,20 @@ function createIconGroup(
343344
items,
344345
onSelect,
345346
}: {
346-
items: TaskItem[]
347-
onSelect: (item: TaskItem) => void
347+
items: FolderedItem[]
348+
onSelect: (item: FolderedItem) => void
348349
}) {
349350
if (items.length === 0) return null
350351
return (
351352
<Command.Group heading={heading} className={GROUP_HEADING_CLASSNAME}>
352353
{items.map((item) => (
353354
<MemoizedIconItem
354355
key={item.id}
355-
value={`${item.name} ${prefix}-${item.id}`}
356+
value={`${item.name} ${item.folderPath?.join(' / ') ?? ''} ${prefix}-${item.id}`}
356357
onSelect={() => onSelect(item)}
357358
name={item.name}
358359
icon={icon}
360+
folderPath={item.folderPath}
359361
/>
360362
))}
361363
</Command.Group>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -621,21 +621,44 @@ export function SearchModal({
621621
}, [isOnWorkflowPage, docs, deferredSearch])
622622

623623
const filteredTables = useMemo(
624-
() => filterAndCap(tables, (t) => t.name, deferredSearch),
624+
() =>
625+
filterAndCap(
626+
tables,
627+
(t) => t.name,
628+
deferredSearch,
629+
(t) => t.folderPath?.join(' ')
630+
),
625631
[tables, deferredSearch]
626632
)
627633
const filteredFiles = useMemo(
628-
() => filterAndCap(files, (f) => `${f.name} ${f.folderPath?.join(' ') ?? ''}`, deferredSearch),
634+
() =>
635+
filterAndCap(
636+
files,
637+
(f) => f.name,
638+
deferredSearch,
639+
(f) => f.folderPath?.join(' ')
640+
),
629641
[files, deferredSearch]
630642
)
631643
const filteredKnowledgeBases = useMemo(
632-
() => filterAndCap(knowledgeBases, (kb) => kb.name, deferredSearch),
644+
() =>
645+
filterAndCap(
646+
knowledgeBases,
647+
(kb) => kb.name,
648+
deferredSearch,
649+
(kb) => kb.folderPath?.join(' ')
650+
),
633651
[knowledgeBases, deferredSearch]
634652
)
635653

636654
const filteredWorkflows = useMemo(
637655
() =>
638-
filterAndCap(workflows, (w) => `${w.name} ${w.folderPath?.join(' ') ?? ''}`, deferredSearch),
656+
filterAndCap(
657+
workflows,
658+
(w) => w.name,
659+
deferredSearch,
660+
(w) => w.folderPath?.join(' ')
661+
),
639662
[workflows, deferredSearch]
640663
)
641664
const filteredChats = useMemo(

0 commit comments

Comments
 (0)