power-compass 0.0.1-alpha.1 → 0.0.1-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/power-compass.cjs.js +20 -0
- package/dist/power-compass.es.d.ts +1 -0
- package/dist/power-compass.es.js +4831 -0
- package/package.json +12 -5
- package/.eslintrc.json +0 -6
- package/index.html +0 -13
- package/jest.config.js +0 -11
- package/src/components/context.ts +0 -9
- package/src/components/hooks/useCursor.ts +0 -38
- package/src/components/hooks/useDebounce.ts +0 -14
- package/src/components/hooks/useShortcut.ts +0 -18
- package/src/http/fetch.ts +0 -30
- package/src/http/index.ts +0 -1
- package/src/index.ts +0 -5
- package/src/menu/clientMenu.ts +0 -56
- package/src/menu/fetchMenu.spec.ts +0 -183
- package/src/menu/fetchMenu.ts +0 -42
- package/src/menu/index.ts +0 -2
- package/src/menu/transformations.ts +0 -48
- package/src/menu/types.ts +0 -19
- package/src/menu/useCompassMenu.spec.tsx +0 -173
- package/src/menu/useCompassMenu.ts +0 -67
- package/src/search/engine.ts +0 -109
- package/src/search/index.ts +0 -1
- package/src/search/sources.ts +0 -232
- package/src/search/types.ts +0 -39
- package/src/search/useSearch.ts +0 -84
- package/src/tasks/index.ts +0 -1
- package/src/tasks/types.ts +0 -7
- package/src/tasks/useLocalStorage.ts +0 -31
- package/src/tasks/useNotificationCenter.ts +0 -69
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -34
- package/vitest.config.ts +0 -8
- package/vitest.fetch-mock.ts +0 -7
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/* global fetchMock */
|
|
2
|
-
|
|
3
|
-
import React from "react"
|
|
4
|
-
import { renderHook } from "@testing-library/react-hooks"
|
|
5
|
-
import { expect, describe, beforeEach, it } from "vitest"
|
|
6
|
-
|
|
7
|
-
import { Compass } from "../components/context"
|
|
8
|
-
|
|
9
|
-
import { useCompassMenu } from "./useCompassMenu"
|
|
10
|
-
import { type MenuItem } from "./types"
|
|
11
|
-
import { type FlatMenuItem } from "./transformations"
|
|
12
|
-
import { createMenuItem } from "./clientMenu"
|
|
13
|
-
|
|
14
|
-
const currentUserId = "123"
|
|
15
|
-
|
|
16
|
-
describe("useCompassMenu", () => {
|
|
17
|
-
const artItem = {
|
|
18
|
-
label: "Art",
|
|
19
|
-
icon: "art",
|
|
20
|
-
url: "http://example.com/art",
|
|
21
|
-
gravity: 1,
|
|
22
|
-
}
|
|
23
|
-
const testItem = {
|
|
24
|
-
label: "Test",
|
|
25
|
-
icon: "test1",
|
|
26
|
-
gravity: 1,
|
|
27
|
-
items: [
|
|
28
|
-
{
|
|
29
|
-
gravity: 1,
|
|
30
|
-
label: "Test 2",
|
|
31
|
-
url: "http://example.com/test2",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
gravity: 1,
|
|
35
|
-
label: "Test 3",
|
|
36
|
-
url: "http://example.com/test3",
|
|
37
|
-
items: [
|
|
38
|
-
{
|
|
39
|
-
gravity: 1,
|
|
40
|
-
label: "Test 4",
|
|
41
|
-
url: "http://example.com/test4",
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
}
|
|
47
|
-
const items: MenuItem[] = [artItem, testItem]
|
|
48
|
-
|
|
49
|
-
beforeEach(() => {
|
|
50
|
-
fetchMock.mockIf(
|
|
51
|
-
`http://example.com/compass/${currentUserId}/menu`,
|
|
52
|
-
JSON.stringify(items),
|
|
53
|
-
)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it("loads items from the given backends", async () => {
|
|
57
|
-
const options = {
|
|
58
|
-
backends: ["http://example.com/compass"],
|
|
59
|
-
contextId: currentUserId,
|
|
60
|
-
}
|
|
61
|
-
const { result, waitForNextUpdate } = renderHook(() =>
|
|
62
|
-
useCompassMenu(options),
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
await waitForNextUpdate()
|
|
66
|
-
|
|
67
|
-
expect(result.current.items).toEqual(items)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it("loads items from the given context backends", async () => {
|
|
71
|
-
const wrapper: React.FC = ({
|
|
72
|
-
children,
|
|
73
|
-
}: {
|
|
74
|
-
children?: React.ReactNode
|
|
75
|
-
}) => (
|
|
76
|
-
<Compass.Provider
|
|
77
|
-
value={{
|
|
78
|
-
backends: ["http://example.com/compass"],
|
|
79
|
-
contextId: currentUserId,
|
|
80
|
-
}}
|
|
81
|
-
>
|
|
82
|
-
{children}
|
|
83
|
-
</Compass.Provider>
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
const { result, waitForNextUpdate } = renderHook(() => useCompassMenu({}), {
|
|
87
|
-
wrapper,
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
await waitForNextUpdate()
|
|
91
|
-
|
|
92
|
-
expect(result.current.items).toEqual(items)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it("flattens the items when using the transformation option", async () => {
|
|
96
|
-
const options = {
|
|
97
|
-
backends: ["http://example.com/compass"],
|
|
98
|
-
contextId: currentUserId,
|
|
99
|
-
flatten: true,
|
|
100
|
-
}
|
|
101
|
-
const { result, waitForNextUpdate } = renderHook(() =>
|
|
102
|
-
useCompassMenu(options),
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
await waitForNextUpdate()
|
|
106
|
-
|
|
107
|
-
const flatItems: FlatMenuItem[] = [
|
|
108
|
-
{
|
|
109
|
-
label: "Art",
|
|
110
|
-
icon: "art",
|
|
111
|
-
url: "http://example.com/art",
|
|
112
|
-
parent: undefined,
|
|
113
|
-
gravity: 1,
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
label: "Test 2",
|
|
117
|
-
url: "http://example.com/test2",
|
|
118
|
-
gravity: 1,
|
|
119
|
-
parent: { label: "Test", icon: "test1", gravity: 1, parent: undefined },
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
label: "Test 4",
|
|
123
|
-
url: "http://example.com/test4",
|
|
124
|
-
gravity: 1,
|
|
125
|
-
parent: {
|
|
126
|
-
label: "Test 3",
|
|
127
|
-
url: "http://example.com/test3",
|
|
128
|
-
gravity: 1,
|
|
129
|
-
parent: {
|
|
130
|
-
label: "Test",
|
|
131
|
-
icon: "test1",
|
|
132
|
-
gravity: 1,
|
|
133
|
-
parent: undefined,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
]
|
|
138
|
-
|
|
139
|
-
expect(result.current.items).toEqual(flatItems)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it("sorts client and remote items together by gravity", async () => {
|
|
143
|
-
const clientFloatingItem = {
|
|
144
|
-
label: "Client Floating Item",
|
|
145
|
-
url: "http://example.com/client-floating",
|
|
146
|
-
gravity: 0,
|
|
147
|
-
}
|
|
148
|
-
const clientItem = {
|
|
149
|
-
label: "Client Item",
|
|
150
|
-
url: "http://example.com/client",
|
|
151
|
-
gravity: 1,
|
|
152
|
-
}
|
|
153
|
-
createMenuItem(clientItem)
|
|
154
|
-
createMenuItem(clientFloatingItem)
|
|
155
|
-
|
|
156
|
-
const options = {
|
|
157
|
-
backends: ["http://example.com/compass"],
|
|
158
|
-
contextId: currentUserId,
|
|
159
|
-
}
|
|
160
|
-
const { result, waitForNextUpdate } = renderHook(() =>
|
|
161
|
-
useCompassMenu(options),
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
await waitForNextUpdate()
|
|
165
|
-
|
|
166
|
-
expect(result.current.items).toEqual([
|
|
167
|
-
clientFloatingItem,
|
|
168
|
-
artItem,
|
|
169
|
-
clientItem,
|
|
170
|
-
testItem,
|
|
171
|
-
])
|
|
172
|
-
})
|
|
173
|
-
})
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { useState, useContext, useEffect } from "react"
|
|
2
|
-
|
|
3
|
-
import { type CompassMenuFilters, fetchMenu } from "./fetchMenu"
|
|
4
|
-
import { type CompassContext, Compass } from "../components/context"
|
|
5
|
-
import {
|
|
6
|
-
type TransformOptions,
|
|
7
|
-
type TransformedMenuItem,
|
|
8
|
-
applyTransformations,
|
|
9
|
-
} from "./transformations"
|
|
10
|
-
import { fetchPlugin } from "./clientMenu"
|
|
11
|
-
|
|
12
|
-
type UseCompassMenuOptions = Partial<CompassContext> &
|
|
13
|
-
TransformOptions & {
|
|
14
|
-
filters?: CompassMenuFilters
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type UseCompassMenu = {
|
|
18
|
-
items: TransformedMenuItem[]
|
|
19
|
-
loading: boolean
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function sortItems(
|
|
23
|
-
resolvedItems: TransformedMenuItem[],
|
|
24
|
-
): TransformedMenuItem[] {
|
|
25
|
-
return resolvedItems.sort(
|
|
26
|
-
(a, b) =>
|
|
27
|
-
(a?.gravity || 0) - (b?.gravity || 0) || a.label.localeCompare(b.label),
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function useCompassMenu({
|
|
32
|
-
filters = {},
|
|
33
|
-
backends: backendsOverride,
|
|
34
|
-
contextId: contextIdOverride,
|
|
35
|
-
...options
|
|
36
|
-
}: UseCompassMenuOptions): UseCompassMenu {
|
|
37
|
-
const [items, setMenuItems] = useState<TransformedMenuItem[]>([])
|
|
38
|
-
const [loading, setLoading] = useState(false)
|
|
39
|
-
const context = useContext(Compass)
|
|
40
|
-
const backends = backendsOverride || context?.backends || []
|
|
41
|
-
const contextId = contextIdOverride || context?.contextId
|
|
42
|
-
if (!contextId) {
|
|
43
|
-
throw new Error("contextId is required")
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
setLoading(true)
|
|
48
|
-
const updatedMenu = [
|
|
49
|
-
...backends.map((backend) =>
|
|
50
|
-
fetchMenu(backend, contextId.toString(), filters),
|
|
51
|
-
),
|
|
52
|
-
fetchPlugin(filters),
|
|
53
|
-
]
|
|
54
|
-
Promise.all(updatedMenu)
|
|
55
|
-
.then((results) => {
|
|
56
|
-
const menuItems = results
|
|
57
|
-
.filter((result) => !result.error)
|
|
58
|
-
.flatMap((result) => result.items)
|
|
59
|
-
return applyTransformations(menuItems, options)
|
|
60
|
-
})
|
|
61
|
-
.then(sortItems)
|
|
62
|
-
.then(setMenuItems)
|
|
63
|
-
.finally(() => setLoading(false))
|
|
64
|
-
}, [])
|
|
65
|
-
|
|
66
|
-
return { items, loading }
|
|
67
|
-
}
|
package/src/search/engine.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import chain from "lodash/chain"
|
|
2
|
-
import sortBy from "lodash/sortBy"
|
|
3
|
-
import Fuse from "fuse.js"
|
|
4
|
-
|
|
5
|
-
import type { ResultEntry, SearchResult, SourceDefinition } from "./types"
|
|
6
|
-
|
|
7
|
-
const DefaultEntriesPerSource = 10
|
|
8
|
-
const RequiredOptions = {
|
|
9
|
-
includeScore: true,
|
|
10
|
-
shouldSort: true,
|
|
11
|
-
}
|
|
12
|
-
const DefaultOptions = {
|
|
13
|
-
keys: [
|
|
14
|
-
{ name: "label", weight: 0.6 },
|
|
15
|
-
{ name: "category", weight: 0.3 },
|
|
16
|
-
{ name: "tag", weight: 1 },
|
|
17
|
-
],
|
|
18
|
-
matchAllTokens: true,
|
|
19
|
-
maxPatternLength: 16,
|
|
20
|
-
minMatchCharLength: 3,
|
|
21
|
-
threshold: 0.4,
|
|
22
|
-
tokenize: true,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Scores and sorts matches.
|
|
27
|
-
*/
|
|
28
|
-
function scoreEntries<T>(
|
|
29
|
-
query: string,
|
|
30
|
-
entries: T[],
|
|
31
|
-
source: SourceDefinition<T>,
|
|
32
|
-
): ResultEntry<T>[] {
|
|
33
|
-
const fuse = new Fuse(entries, {
|
|
34
|
-
...DefaultOptions,
|
|
35
|
-
...source.searchOptions,
|
|
36
|
-
...RequiredOptions,
|
|
37
|
-
})
|
|
38
|
-
return chain(fuse.search(query))
|
|
39
|
-
.map(({ item, score }: ResultEntry<T>) => ({ item, score, source: source }))
|
|
40
|
-
.take(source.maxEntries || DefaultEntriesPerSource)
|
|
41
|
-
.value() as ResultEntry<T>[]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Sorts sources by best match's score.
|
|
46
|
-
*
|
|
47
|
-
* @returns {[SearchResult, SearchResult]} sources without `insertSourceAtIndex` and sources with `insertSourceAtIndex`.
|
|
48
|
-
*/
|
|
49
|
-
function partitionGroups<T>(
|
|
50
|
-
data: SearchResult<T>[],
|
|
51
|
-
): [SearchResult<T>[], SearchResult<T>[]] {
|
|
52
|
-
return chain(data)
|
|
53
|
-
.sortBy("entries[0].score")
|
|
54
|
-
.partition(({ source }) => source.insertSourceAtIndex === undefined)
|
|
55
|
-
.value()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Sorts matches by score.
|
|
60
|
-
* Sorts sources by best match's score and move them as per specified by their `insertSourceAtIndex` key.
|
|
61
|
-
*
|
|
62
|
-
* @param {*} sources all sources added to Spotnitro. See `app/javascript/packs/spotnitro.ts`
|
|
63
|
-
* @param {*} data GraphQL data
|
|
64
|
-
* @param {*} search user's input
|
|
65
|
-
* @see source-scores-spec.js
|
|
66
|
-
*/
|
|
67
|
-
export function sortSearchResults<T>(
|
|
68
|
-
results: SearchResult<T>[],
|
|
69
|
-
): SearchResult<T>[] {
|
|
70
|
-
const [sortedGroups, groupsToInsertAtIndex] = partitionGroups(results)
|
|
71
|
-
|
|
72
|
-
return sortBy(groupsToInsertAtIndex, "source.insertSourceAtIndex").reduce(
|
|
73
|
-
(groups, group) => [
|
|
74
|
-
...groups.slice(0, group.source.insertSourceAtIndex),
|
|
75
|
-
group,
|
|
76
|
-
...groups.slice(group.source.insertSourceAtIndex),
|
|
77
|
-
],
|
|
78
|
-
sortedGroups,
|
|
79
|
-
) as SearchResult<T>[]
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
type OnResultCallback = (result: SearchResult<unknown>) => void
|
|
83
|
-
|
|
84
|
-
export function search(
|
|
85
|
-
value: string,
|
|
86
|
-
sources: SourceDefinition<unknown>[],
|
|
87
|
-
abortController?: AbortController,
|
|
88
|
-
onResult?: OnResultCallback,
|
|
89
|
-
): Promise<SearchResult<unknown>[]> {
|
|
90
|
-
const promises = sources.map(
|
|
91
|
-
(source: SourceDefinition<unknown>): Promise<SearchResult<unknown>> =>
|
|
92
|
-
source.fetch(value, abortController).then(
|
|
93
|
-
(entries: unknown[]) => {
|
|
94
|
-
const result = {
|
|
95
|
-
source,
|
|
96
|
-
entries: scoreEntries(value, entries, source),
|
|
97
|
-
}
|
|
98
|
-
onResult?.call(null, result)
|
|
99
|
-
return result
|
|
100
|
-
},
|
|
101
|
-
(error) => {
|
|
102
|
-
console.debug(`Failed to search on source: ${source.label}`, error)
|
|
103
|
-
return { source, entries: [] as ResultEntry<unknown>[] }
|
|
104
|
-
},
|
|
105
|
-
),
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
return Promise.all(promises)
|
|
109
|
-
}
|
package/src/search/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useSearch"
|
package/src/search/sources.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import get from "lodash/get"
|
|
2
|
-
|
|
3
|
-
import { fetchMenu } from "../menu/fetchMenu"
|
|
4
|
-
import { type CompassMenuResult } from "../menu/types"
|
|
5
|
-
import { type SourceDefinition } from "./types"
|
|
6
|
-
|
|
7
|
-
export const sources: SourceDefinition<unknown>[] = []
|
|
8
|
-
|
|
9
|
-
type SourceDefinitionCreation<T> = Omit<
|
|
10
|
-
SourceDefinition<T>,
|
|
11
|
-
"open" | "display"
|
|
12
|
-
> &
|
|
13
|
-
Partial<Pick<SourceDefinition<T>, "open" | "display">>
|
|
14
|
-
|
|
15
|
-
export async function createCompassProviderSource(
|
|
16
|
-
contextId: string | number | undefined,
|
|
17
|
-
providerName: string,
|
|
18
|
-
search: string,
|
|
19
|
-
abortController?: AbortController,
|
|
20
|
-
): Promise<SourceDefinition<unknown> | null> {
|
|
21
|
-
if (!search || search.length < 3 || !contextId || !providerName) return null
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const response = await fetch(
|
|
25
|
-
`/compass/${contextId}/search/${providerName}?q=${encodeURIComponent(search)}`,
|
|
26
|
-
{ signal: abortController?.signal },
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
if (!response.ok) {
|
|
30
|
-
console.debug(`Failed to fetch ${providerName} search results:`, response)
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const data = await response.json()
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
label: data.label || providerName,
|
|
38
|
-
searchOptions: data.search_options || { threshold: 0.2 },
|
|
39
|
-
fetch: () => Promise.resolve(data.results || []),
|
|
40
|
-
display: {
|
|
41
|
-
label: (item: any) => item.label ?? "",
|
|
42
|
-
category: (item: any) => item.category ?? "",
|
|
43
|
-
tag: (item: any) => item.tag ?? "",
|
|
44
|
-
},
|
|
45
|
-
open: (item: any) => {
|
|
46
|
-
if (item.requires_confirmation && item.data) {
|
|
47
|
-
if (!window.confirm(item.data)) {
|
|
48
|
-
return null
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return item.url
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if ((error as Error).name !== "AbortError") {
|
|
56
|
-
console.debug(`Error creating compass ${providerName} source:`, error)
|
|
57
|
-
}
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function getCompassProviders(
|
|
63
|
-
contextId: string | number,
|
|
64
|
-
abortController?: AbortController,
|
|
65
|
-
): Promise<string[]> {
|
|
66
|
-
try {
|
|
67
|
-
const response = await fetch(`/compass/${contextId}/search/providers`, {
|
|
68
|
-
signal: abortController?.signal,
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
if (!response.ok) {
|
|
72
|
-
console.debug("Failed to fetch providers:", response.status)
|
|
73
|
-
return []
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const data = await response.json()
|
|
77
|
-
|
|
78
|
-
return data.providers?.map((provider: any) => provider.name) || []
|
|
79
|
-
} catch (error) {
|
|
80
|
-
if ((error as Error).name !== "AbortError") {
|
|
81
|
-
console.debug("Error fetching available providers:", error)
|
|
82
|
-
}
|
|
83
|
-
return []
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function createCompassSources(
|
|
88
|
-
contextId: string | number | undefined,
|
|
89
|
-
search: string,
|
|
90
|
-
abortController?: AbortController,
|
|
91
|
-
): Promise<SourceDefinition<unknown>[]> {
|
|
92
|
-
if (!search || search.length < 3 || !contextId) return []
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const providerNames = await getCompassProviders(contextId, abortController)
|
|
96
|
-
|
|
97
|
-
if (!providerNames.length) {
|
|
98
|
-
console.debug("No providers available")
|
|
99
|
-
return []
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
console.debug(`Found ${providerNames.length} providers:`, providerNames)
|
|
103
|
-
|
|
104
|
-
const promises = providerNames.map((providerName) =>
|
|
105
|
-
createCompassProviderSource(
|
|
106
|
-
contextId,
|
|
107
|
-
providerName,
|
|
108
|
-
search,
|
|
109
|
-
abortController,
|
|
110
|
-
),
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
const sources = await Promise.all(promises)
|
|
114
|
-
|
|
115
|
-
const validSources = sources.filter(
|
|
116
|
-
(source): source is SourceDefinition<unknown> => source !== null,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
console.debug(
|
|
120
|
-
`Successfully created ${validSources.length}/${providerNames.length} sources`,
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
return validSources
|
|
124
|
-
} catch (error) {
|
|
125
|
-
if ((error as Error).name !== "AbortError") {
|
|
126
|
-
console.debug("Error creating compass individual sources:", error)
|
|
127
|
-
}
|
|
128
|
-
return []
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function createMenuSources(
|
|
133
|
-
backends: string[],
|
|
134
|
-
contextId: string,
|
|
135
|
-
): SourceDefinition<unknown>[] {
|
|
136
|
-
if (!backends || !backends.length || !contextId) {
|
|
137
|
-
return []
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return backends.map((backend) => {
|
|
141
|
-
return {
|
|
142
|
-
fetch: async (_search: string) => {
|
|
143
|
-
try {
|
|
144
|
-
const menuResult: CompassMenuResult = await fetchMenu(
|
|
145
|
-
backend,
|
|
146
|
-
contextId,
|
|
147
|
-
)
|
|
148
|
-
if (menuResult.error) {
|
|
149
|
-
return []
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const searchableItems: Array<{
|
|
153
|
-
label: string
|
|
154
|
-
url: string
|
|
155
|
-
category: string
|
|
156
|
-
icon?: string
|
|
157
|
-
}> = []
|
|
158
|
-
|
|
159
|
-
menuResult.items.forEach((topItem: any) => {
|
|
160
|
-
const category = topItem.label
|
|
161
|
-
const icon = topItem.icon
|
|
162
|
-
|
|
163
|
-
if (topItem.url) {
|
|
164
|
-
searchableItems.push({
|
|
165
|
-
label: topItem.label,
|
|
166
|
-
url: topItem.url,
|
|
167
|
-
category: category,
|
|
168
|
-
icon: icon,
|
|
169
|
-
})
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (topItem.items && Array.isArray(topItem.items)) {
|
|
173
|
-
topItem.items.forEach((item: any) => {
|
|
174
|
-
searchableItems.push({
|
|
175
|
-
label: item.label,
|
|
176
|
-
url: item.url,
|
|
177
|
-
category: category,
|
|
178
|
-
icon: item.icon || icon,
|
|
179
|
-
})
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
return searchableItems
|
|
185
|
-
} catch (error) {
|
|
186
|
-
return []
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
display: {
|
|
191
|
-
label: (item: any) => {
|
|
192
|
-
return item.label
|
|
193
|
-
},
|
|
194
|
-
category: (item: any) => {
|
|
195
|
-
return item.category
|
|
196
|
-
},
|
|
197
|
-
tag: () => {
|
|
198
|
-
return ""
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
open: ({ url, label }: { url: string; label: string }) => {
|
|
203
|
-
const fullUrl = new URL(url, document.location.href)
|
|
204
|
-
fullUrl.searchParams.append("mt", label)
|
|
205
|
-
return fullUrl.toString()
|
|
206
|
-
},
|
|
207
|
-
searchOptions: { threshold: 0.2 },
|
|
208
|
-
maxEntries: 20,
|
|
209
|
-
insertSourceAtIndex: 0,
|
|
210
|
-
} as SourceDefinition<unknown>
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function createSource<T>(source: SourceDefinitionCreation<T>) {
|
|
215
|
-
sources.push({
|
|
216
|
-
open(obj: T) {
|
|
217
|
-
return get(obj, "url")
|
|
218
|
-
},
|
|
219
|
-
display: {
|
|
220
|
-
label(obj: T) {
|
|
221
|
-
return get(obj, "label")
|
|
222
|
-
},
|
|
223
|
-
category(obj: T) {
|
|
224
|
-
return get(obj, "category")
|
|
225
|
-
},
|
|
226
|
-
tag(obj: T) {
|
|
227
|
-
return get(obj, "tag")
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
...source,
|
|
231
|
-
})
|
|
232
|
-
}
|
package/src/search/types.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type ComponentType } from "react"
|
|
2
|
-
|
|
3
|
-
export type SourceDefinition<T> = {
|
|
4
|
-
label?: string
|
|
5
|
-
tag?: string
|
|
6
|
-
searchOptions?: Record<string, unknown>
|
|
7
|
-
newTab?: boolean
|
|
8
|
-
insertSourceAtIndex?: number
|
|
9
|
-
maxEntries?: number
|
|
10
|
-
|
|
11
|
-
display: ComponentType<ResultEntry<T>> | DisplayProps<T>
|
|
12
|
-
|
|
13
|
-
fetch: (arg0: string, options?: RequestInit) => Promise<T[]>
|
|
14
|
-
open: (arg0: T) => string | Promise<string> | void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type ResultEntry<T> = {
|
|
18
|
-
item: T
|
|
19
|
-
score: number
|
|
20
|
-
source: SourceDefinition<T>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type SearchResult<T> = {
|
|
24
|
-
source: SourceDefinition<T>
|
|
25
|
-
entries: ResultEntry<T>[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface SearchItem {
|
|
29
|
-
html?: string
|
|
30
|
-
label?: string
|
|
31
|
-
tag?: string
|
|
32
|
-
url?: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export type DisplayProps<T> = {
|
|
36
|
-
label: (arg0: T) => Promise<string> | string
|
|
37
|
-
category: (arg0: T) => Promise<string> | string
|
|
38
|
-
tag: (arg0: T) => Promise<string> | string
|
|
39
|
-
}
|
package/src/search/useSearch.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useCallback,
|
|
3
|
-
useEffect,
|
|
4
|
-
useRef,
|
|
5
|
-
useState,
|
|
6
|
-
useContext,
|
|
7
|
-
useMemo,
|
|
8
|
-
} from "react"
|
|
9
|
-
import { sortSearchResults, search } from "./engine"
|
|
10
|
-
import { type SearchResult } from "./types"
|
|
11
|
-
import { Compass } from "../components/context"
|
|
12
|
-
import { createMenuSources, createCompassSources } from "./sources"
|
|
13
|
-
|
|
14
|
-
type UseSpotnitroReturn = [SearchResult<unknown>[], boolean, boolean]
|
|
15
|
-
|
|
16
|
-
export function useSearch(value?: string): UseSpotnitroReturn {
|
|
17
|
-
const [results, setResults] = useState<SearchResult<unknown>[]>([])
|
|
18
|
-
const [loading, setLoading] = useState(false)
|
|
19
|
-
const [finished, setFinished] = useState(false)
|
|
20
|
-
const compassContext = useContext(Compass)
|
|
21
|
-
const backends = compassContext?.backends
|
|
22
|
-
const contextId = compassContext?.contextId
|
|
23
|
-
const abort = useRef<AbortController | undefined>(undefined)
|
|
24
|
-
const finish = useCallback(() => {
|
|
25
|
-
setLoading(false)
|
|
26
|
-
setFinished(true)
|
|
27
|
-
}, [])
|
|
28
|
-
const menuSources = useMemo(
|
|
29
|
-
() =>
|
|
30
|
-
contextId && backends
|
|
31
|
-
? createMenuSources(backends, contextId.toString())
|
|
32
|
-
: [],
|
|
33
|
-
[contextId, backends],
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
const updateScoredResults = useCallback((result: SearchResult<unknown>) => {
|
|
37
|
-
if (result.entries.length > 0) {
|
|
38
|
-
setResults((results) => {
|
|
39
|
-
const filteredResults = results.filter(
|
|
40
|
-
(r) => r.source.label !== result.source.label,
|
|
41
|
-
)
|
|
42
|
-
return sortSearchResults([result, ...filteredResults])
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
}, [])
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
const performSearch = async () => {
|
|
49
|
-
if (!value || value.length < 3) {
|
|
50
|
-
setResults([])
|
|
51
|
-
setLoading(false)
|
|
52
|
-
setFinished(false)
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
abort.current?.abort()
|
|
57
|
-
abort.current = new AbortController()
|
|
58
|
-
setFinished(false)
|
|
59
|
-
setResults([])
|
|
60
|
-
setLoading(true)
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const allIndividualSources = await createCompassSources(
|
|
64
|
-
contextId,
|
|
65
|
-
value,
|
|
66
|
-
abort.current,
|
|
67
|
-
)
|
|
68
|
-
const allSources = [...allIndividualSources, ...menuSources]
|
|
69
|
-
|
|
70
|
-
await search(value, allSources, abort.current, updateScoredResults)
|
|
71
|
-
} catch (error) {
|
|
72
|
-
if ((error as Error).name !== "AbortError") {
|
|
73
|
-
console.debug("Search failed:", error)
|
|
74
|
-
}
|
|
75
|
-
} finally {
|
|
76
|
-
finish()
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
performSearch()
|
|
81
|
-
}, [value, contextId, backends, menuSources, updateScoredResults, finish])
|
|
82
|
-
|
|
83
|
-
return [results, loading, finished]
|
|
84
|
-
}
|
package/src/tasks/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./useNotificationCenter"
|