Skip to content

Commit 9c9bf4e

Browse files
committed
feat(global-search): add indexer support for CollectionItem
1 parent e2fd50d commit 9c9bf4e

4 files changed

Lines changed: 114 additions & 44 deletions

File tree

plugins/global-search/src/components/DevToolsScene.tsx

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ export function DevToolsScene() {
1515
const filteredEntries = useMemo(
1616
() =>
1717
entries.filter(entry => {
18-
if (entry.node.id === filterQuery) return true
19-
return entry.name?.toLowerCase().includes(filterQuery.toLowerCase())
18+
if (entry.id === filterQuery) return true
19+
switch (entry.type) {
20+
case "CollectionItem":
21+
return Object.values(entry.fields).some(field =>
22+
field.toLowerCase().includes(filterQuery.toLowerCase())
23+
)
24+
default:
25+
return entry.name?.toLowerCase().includes(filterQuery.toLowerCase())
26+
}
2027
}),
2128
[entries, filterQuery]
2229
)
@@ -31,7 +38,6 @@ export function DevToolsScene() {
3138
},
3239
{} as Record<string, number>
3340
),
34-
withText: entries.filter(e => e.text).length,
3541
}),
3642
[entries]
3743
)
@@ -79,12 +85,11 @@ export function DevToolsScene() {
7985
<div className="flex items-start justify-between">
8086
<div className="flex-1 min-w-0">
8187
<p className="text-sm font-medium text-gray-900 truncate">
82-
{entry.name || "Unnamed"}
88+
{entry.type === "CollectionItem"
89+
? `${entry.rootNodeName} - ${entry.slug}`
90+
: entry.name || "Unnamed"}
8391
</p>
8492
<p className="text-xs text-gray-500">{entry.type}</p>
85-
{entry.text && (
86-
<p className="text-xs text-gray-600 mt-1 line-clamp-2">{entry.text}</p>
87-
)}
8893
</div>
8994
</div>
9095
</div>
@@ -116,11 +121,13 @@ export function DevToolsScene() {
116121
<div>
117122
<label className="block text-xs font-medium text-gray-700 mb-1">Name</label>
118123
<p className="text-sm bg-gray-50 p-2 rounded">
119-
{selectedEntry.name || "(no name)"}
124+
{selectedEntry.type === "CollectionItem"
125+
? "Collection Item"
126+
: selectedEntry.name || "(no name)"}
120127
</p>
121128
</div>
122129

123-
{selectedEntry.text && (
130+
{selectedEntry.type !== "CollectionItem" && (
124131
<div>
125132
<label className="block text-xs font-medium text-gray-700 mb-1">
126133
Text Content
@@ -131,6 +138,15 @@ export function DevToolsScene() {
131138
</div>
132139
)}
133140

141+
{selectedEntry.type === "CollectionItem" && (
142+
<div>
143+
<label className="block text-xs font-medium text-gray-700 mb-1">Fields</label>
144+
<pre className="text-sm bg-gray-50 p-2 rounded whitespace-pre-wrap">
145+
{JSON.stringify(selectedEntry.fields, null, 2)}
146+
</pre>
147+
</div>
148+
)}
149+
134150
<div>
135151
<label className="block text-xs font-medium text-gray-700 mb-1">Root Node</label>
136152
<p className="text-sm bg-gray-50 p-2 rounded">
@@ -149,15 +165,9 @@ export function DevToolsScene() {
149165
<pre className="mt-2 bg-gray-100 p-2 rounded overflow-auto text-xs">
150166
{JSON.stringify(
151167
{
152-
...selectedEntry.node,
153-
// Limit some potentially large properties
154-
...(selectedEntry.node.__class && {
155-
__class: selectedEntry.node.__class,
156-
}),
157-
...(selectedEntry.node.id && { id: selectedEntry.node.id }),
158-
...("name" in selectedEntry.node && {
159-
name: selectedEntry.node.name,
160-
}),
168+
...(selectedEntry.type === "CollectionItem"
169+
? selectedEntry.collectionItem
170+
: selectedEntry.node),
161171
},
162172
null,
163173
2
File renamed without changes.

plugins/global-search/src/utils/indexer/indexer.ts

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import { type AnyNode, framer, isComponentNode, isFrameNode, isTextNode, isWebPageNode } from "framer-plugin"
2-
import { TypedEventEmitter } from "./event-emitter"
1+
import {
2+
type AnyNode,
3+
Collection,
4+
framer,
5+
isComponentNode,
6+
isFrameNode,
7+
isTextNode,
8+
isWebPageNode,
9+
} from "framer-plugin"
10+
import { type EventMap, TypedEventEmitter } from "../event-emitter"
311
import { stripMarkup } from "./strip-markup"
412
import { type IndexEntry, includedAttributes, type RootNode, shouldIndexNode } from "./types"
513

@@ -58,24 +66,11 @@ export class GlobalSearchIndexer {
5866
return null
5967
}
6068

61-
private async *crawl(rootNodes: readonly RootNode[]): AsyncGenerator<IndexEntry[]> {
69+
private async *crawlNodes(rootNodes: readonly RootNode[]): AsyncGenerator<IndexEntry[]> {
6270
let batch: IndexEntry[] = []
6371

6472
for (const rootNode of rootNodes) {
6573
const rootNodeName = await getNodeName(rootNode)
66-
if (shouldIndexNode(rootNode)) {
67-
const text = await this.getNodeText(rootNode)
68-
batch.push({
69-
id: rootNode.id,
70-
type: rootNode.__class,
71-
name: rootNodeName,
72-
text,
73-
node: rootNode,
74-
rootNode,
75-
rootNodeName,
76-
rootNodeType: rootNode.__class,
77-
})
78-
}
7974

8075
for await (const node of rootNode.walk()) {
8176
if (this.abortRequested) return
@@ -107,6 +102,50 @@ export class GlobalSearchIndexer {
107102
}
108103
}
109104

105+
private async *crawlCollections(collections: readonly Collection[]): AsyncGenerator<IndexEntry[]> {
106+
let batch: IndexEntry[] = []
107+
108+
for (const collection of collections) {
109+
const [fieldNames, items] = await Promise.all([collection.getFields(), collection.getItems()])
110+
const fieldNameMap = new Map(fieldNames.map(f => [f.id, f.name]))
111+
112+
for (const item of items) {
113+
const fields = Object.fromEntries(
114+
Object.entries(item.fieldData).flatMap(([key, field]) => {
115+
const finalKey = fieldNameMap.get(key) ?? key
116+
switch (field.type) {
117+
case "string":
118+
return [[finalKey, field.value]]
119+
case "formattedText":
120+
return [[finalKey, stripMarkup(field.value)]]
121+
}
122+
return []
123+
})
124+
)
125+
126+
batch.push({
127+
id: item.id,
128+
type: "CollectionItem",
129+
collectionItem: item,
130+
rootNode: collection,
131+
rootNodeName: collection.name,
132+
rootNodeType: "Collection",
133+
fields,
134+
slug: item.slug,
135+
})
136+
}
137+
138+
if (batch.length === this.batchSize) {
139+
yield batch
140+
batch = []
141+
}
142+
}
143+
144+
if (batch.length > 0) {
145+
yield batch
146+
}
147+
}
148+
110149
async start() {
111150
try {
112151
const [pages, components] = await Promise.all([
@@ -117,7 +156,14 @@ export class GlobalSearchIndexer {
117156
this.abortRequested = false
118157
this.eventEmitter.emit("started")
119158

120-
for await (const batch of this.crawl([...pages, ...components])) {
159+
for await (const batch of this.crawlNodes([...pages, ...components])) {
160+
if (this.abortRequested) break
161+
this.upsertEntries(batch)
162+
}
163+
164+
const collections = await framer.getCollections()
165+
166+
for await (const batch of this.crawlCollections(collections)) {
121167
if (this.abortRequested) break
122168
this.upsertEntries(batch)
123169
}

plugins/global-search/src/utils/indexer/types.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
22
type AnyNode,
3+
Collection,
4+
CollectionItem,
35
ComponentNode,
46
isComponentInstanceNode,
57
isComponentNode,
@@ -13,23 +15,35 @@ import {
1315
export type RootNode = ComponentNode | WebPageNode
1416
export type RootNodeType = RootNode["__class"]
1517

16-
// Search index entry - extends the renamer's IndexEntry concept
17-
export interface IndexEntry {
18+
export type IndexEntryType = AnyNode["__class"]
19+
20+
interface IndexEntryBase {
1821
id: string
19-
type: AnyNode["__class"]
20-
name: string | null
21-
text: string | null
22+
type: string
23+
}
24+
25+
export interface IndexNodeEntry extends IndexEntryBase {
26+
type: IndexEntryType
2227
node: AnyNode
2328
rootNodeName: string | null
29+
rootNode: RootNode
2430
rootNodeType: RootNodeType
25-
rootNode: AnyNode
31+
text: string | null
32+
name: string | null
2633
}
2734

28-
export interface TextRange {
29-
start: number
30-
end: number
35+
export interface IndexCollectionItemEntry extends IndexEntryBase {
36+
type: "CollectionItem"
37+
collectionItem: CollectionItem
38+
rootNodeName: string
39+
rootNode: Collection
40+
rootNodeType: "Collection"
41+
slug: string
42+
fields: Record<string, string>
3143
}
3244

45+
export type IndexEntry = IndexNodeEntry | IndexCollectionItemEntry
46+
3347
export const includedAttributes = ["text"] as const
3448
export type IncludedAttribute = (typeof includedAttributes)[number]
3549

0 commit comments

Comments
 (0)