@dxos/plugin-search 0.9.1-staging.ee54ba693a → 0.11.0
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/PLUGIN.mdl +8 -16
- package/dist/lib/SearchPlugin.mjs +2 -0
- package/dist/lib/capabilities.mjs +9 -0
- package/dist/lib/capabilities.mjs.map +1 -0
- package/dist/lib/chunk-SearchArticle.mjs +43 -0
- package/dist/lib/chunk-SearchArticle.mjs.map +1 -0
- package/dist/lib/chunk-SearchDialog.mjs +62 -0
- package/dist/lib/chunk-SearchDialog.mjs.map +1 -0
- package/dist/lib/chunk-SearchPlugin.mjs +17 -0
- package/dist/lib/chunk-SearchPlugin.mjs.map +1 -0
- package/dist/lib/chunk-app-graph-builder.mjs +52 -0
- package/dist/lib/chunk-app-graph-builder.mjs.map +1 -0
- package/dist/lib/{neutral/chunk-H4PKSTAJ.mjs → chunk-constants.mjs} +4 -5
- package/dist/lib/chunk-constants.mjs.map +1 -0
- package/dist/lib/chunk-hooks.mjs +281 -0
- package/dist/lib/chunk-hooks.mjs.map +1 -0
- package/dist/lib/chunk-meta.mjs +40 -0
- package/dist/lib/chunk-meta.mjs.map +1 -0
- package/dist/lib/chunk-open-search.mjs +16 -0
- package/dist/lib/chunk-open-search.mjs.map +1 -0
- package/dist/lib/chunk-operation-handler.mjs +11 -0
- package/dist/lib/chunk-operation-handler.mjs.map +1 -0
- package/dist/lib/chunk-react-surface.mjs +54 -0
- package/dist/lib/chunk-react-surface.mjs.map +1 -0
- package/dist/lib/chunk-types.mjs +32 -0
- package/dist/lib/chunk-types.mjs.map +1 -0
- package/dist/lib/components.mjs +148 -0
- package/dist/lib/components.mjs.map +1 -0
- package/dist/lib/containers.mjs +8 -0
- package/dist/lib/containers.mjs.map +1 -0
- package/dist/lib/hooks.mjs +2 -0
- package/dist/lib/index.mjs +4 -0
- package/dist/lib/meta.mjs +2 -0
- package/dist/lib/operations.mjs +7 -0
- package/dist/lib/operations.mjs.map +1 -0
- package/dist/lib/plugin.mjs +9 -0
- package/dist/lib/plugin.mjs.map +1 -0
- package/dist/lib/testing.mjs +2 -0
- package/dist/lib/translations.mjs +18 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/types/src/capabilities/SearchSurfaces.d.ts +14 -0
- package/dist/types/src/capabilities/SearchSurfaces.d.ts.map +1 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -1
- package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/components/SearchResultList/SearchResultList.d.ts +15 -0
- package/dist/types/src/components/SearchResultList/SearchResultList.d.ts.map +1 -0
- package/dist/types/src/components/SearchResultList/SearchResultList.stories.d.ts +16 -0
- package/dist/types/src/components/SearchResultList/SearchResultList.stories.d.ts.map +1 -0
- package/dist/types/src/components/SearchResultList/SearchResultList.test.d.ts +2 -0
- package/dist/types/src/components/SearchResultList/SearchResultList.test.d.ts.map +1 -0
- package/dist/types/src/components/SearchResultList/index.d.ts +2 -0
- package/dist/types/src/components/SearchResultList/index.d.ts.map +1 -0
- package/dist/types/src/components/SearchResultStack/SearchResultStack.d.ts +2 -1
- package/dist/types/src/components/SearchResultStack/SearchResultStack.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/containers/SearchArticle/SearchArticle.d.ts +4 -1
- package/dist/types/src/containers/SearchArticle/SearchArticle.d.ts.map +1 -1
- package/dist/types/src/containers/SearchDialog/SearchDialog.d.ts +4 -1
- package/dist/types/src/containers/SearchDialog/SearchDialog.d.ts.map +1 -1
- package/dist/types/src/containers/SearchDialog/SearchDialog.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/SearchContext.d.ts +10 -0
- package/dist/types/src/hooks/SearchContext.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +3 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/search-query.d.ts +26 -0
- package/dist/types/src/hooks/search-query.d.ts.map +1 -0
- package/dist/types/src/hooks/search-query.test.d.ts +2 -0
- package/dist/types/src/hooks/search-query.test.d.ts.map +1 -0
- package/dist/types/src/hooks/sync.d.ts +2 -1
- package/dist/types/src/hooks/sync.d.ts.map +1 -1
- package/dist/types/src/hooks/useGlobalSearch.d.ts +0 -9
- package/dist/types/src/hooks/useGlobalSearch.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dx.config.ts +5 -7
- package/package.json +47 -44
- package/src/capabilities/SearchSurfaces.tsx +58 -0
- package/src/capabilities/app-graph-builder.ts +4 -4
- package/src/capabilities/react-surface.ts +40 -0
- package/src/components/SearchResultList/SearchResultList.stories.tsx +152 -0
- package/src/components/SearchResultList/SearchResultList.test.tsx +64 -0
- package/src/components/SearchResultList/SearchResultList.tsx +60 -0
- package/src/components/SearchResultList/index.ts +5 -0
- package/src/components/SearchResultStack/SearchResultStack.tsx +15 -8
- package/src/components/index.ts +1 -0
- package/src/containers/SearchArticle/SearchArticle.tsx +10 -18
- package/src/containers/SearchDialog/SearchDialog.stories.tsx +5 -4
- package/src/containers/SearchDialog/SearchDialog.tsx +10 -16
- package/src/hooks/SearchContext.ts +31 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/search-query.test.ts +27 -0
- package/src/hooks/search-query.ts +85 -0
- package/src/hooks/sync.ts +1 -1
- package/src/hooks/useGlobalSearch.tsx +2 -23
- package/src/search/exa.ts +1 -1
- package/src/translations.ts +2 -0
- package/dist/lib/neutral/SearchArticle-XEP4DYSI.mjs +0 -51
- package/dist/lib/neutral/SearchArticle-XEP4DYSI.mjs.map +0 -7
- package/dist/lib/neutral/SearchDialog-37ZDKFPS.mjs +0 -76
- package/dist/lib/neutral/SearchDialog-37ZDKFPS.mjs.map +0 -7
- package/dist/lib/neutral/SearchPlugin.mjs +0 -10
- package/dist/lib/neutral/SearchPlugin.mjs.map +0 -7
- package/dist/lib/neutral/app-graph-builder-43LAJI5B.mjs +0 -71
- package/dist/lib/neutral/app-graph-builder-43LAJI5B.mjs.map +0 -7
- package/dist/lib/neutral/capabilities/index.mjs +0 -13
- package/dist/lib/neutral/capabilities/index.mjs.map +0 -7
- package/dist/lib/neutral/chunk-2JPRW3GN.mjs +0 -50
- package/dist/lib/neutral/chunk-2JPRW3GN.mjs.map +0 -7
- package/dist/lib/neutral/chunk-BODTE4FK.mjs +0 -28
- package/dist/lib/neutral/chunk-BODTE4FK.mjs.map +0 -7
- package/dist/lib/neutral/chunk-E2F6MF44.mjs +0 -8
- package/dist/lib/neutral/chunk-E2F6MF44.mjs.map +0 -7
- package/dist/lib/neutral/chunk-H4PKSTAJ.mjs.map +0 -7
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-KY3X3WAN.mjs +0 -282
- package/dist/lib/neutral/chunk-KY3X3WAN.mjs.map +0 -7
- package/dist/lib/neutral/chunk-P6LRQZVR.mjs +0 -34
- package/dist/lib/neutral/chunk-P6LRQZVR.mjs.map +0 -7
- package/dist/lib/neutral/components/index.mjs +0 -99
- package/dist/lib/neutral/components/index.mjs.map +0 -7
- package/dist/lib/neutral/containers/index.mjs +0 -11
- package/dist/lib/neutral/containers/index.mjs.map +0 -7
- package/dist/lib/neutral/hooks/index.mjs +0 -30
- package/dist/lib/neutral/hooks/index.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -40
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/meta.mjs +0 -10
- package/dist/lib/neutral/meta.mjs.map +0 -7
- package/dist/lib/neutral/open-search-STQH3TXW.mjs +0 -23
- package/dist/lib/neutral/open-search-STQH3TXW.mjs.map +0 -7
- package/dist/lib/neutral/operation-handler-DY624P2R.mjs +0 -13
- package/dist/lib/neutral/operation-handler-DY624P2R.mjs.map +0 -7
- package/dist/lib/neutral/operations/index.mjs +0 -8
- package/dist/lib/neutral/operations/index.mjs.map +0 -7
- package/dist/lib/neutral/plugin.mjs +0 -16
- package/dist/lib/neutral/plugin.mjs.map +0 -7
- package/dist/lib/neutral/react-surface-VQXLVMC2.mjs +0 -60
- package/dist/lib/neutral/react-surface-VQXLVMC2.mjs.map +0 -7
- package/dist/lib/neutral/testing.mjs +0 -8
- package/dist/lib/neutral/testing.mjs.map +0 -7
- package/dist/lib/neutral/translations.mjs +0 -25
- package/dist/lib/neutral/translations.mjs.map +0 -7
- package/dist/lib/neutral/types/index.mjs +0 -8
- package/dist/lib/neutral/types/index.mjs.map +0 -7
- package/src/capabilities/react-surface.tsx +0 -71
package/dx.config.ts
CHANGED
|
@@ -11,19 +11,17 @@ export default Config2.make({
|
|
|
11
11
|
name: 'Search',
|
|
12
12
|
author: 'DXOS',
|
|
13
13
|
description: trim`
|
|
14
|
-
Full-text search
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Full-text search over the active space in DXOS Composer. The plugin provides a global
|
|
15
|
+
command-palette style dialog (invoked via the OpenSearch operation) that debounces user input
|
|
16
|
+
and matches it against the space's FTS5 index, returning ranked, highlighted results with
|
|
17
|
+
field-level match context.
|
|
18
18
|
|
|
19
19
|
A persistent SearchArticle panel can be pinned in the deck companion sidebar so the query stays
|
|
20
20
|
visible while navigating documents. Both surfaces share a SearchContextProvider that drives the
|
|
21
21
|
GlobalFilterProvider, allowing any view that uses useGlobalFilteredObjects to automatically show
|
|
22
22
|
only the objects matching the current query.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
can fetch and rank external web results alongside local objects. The results are structured with
|
|
26
|
-
URL, title, and snippet fields ready for use in Composer documents.
|
|
24
|
+
The mailbox also uses the same full-text index to filter messages by the current search query.
|
|
27
25
|
|
|
28
26
|
All search operations are registered through the standard DXOS operations system and can be
|
|
29
27
|
wired into keyboard shortcuts, command palettes, and AI assistants via the OperationHandlerSet.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/plugin-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Search plugin",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -16,70 +16,70 @@
|
|
|
16
16
|
"#capabilities": {
|
|
17
17
|
"source": "./src/capabilities/index.ts",
|
|
18
18
|
"types": "./dist/types/src/capabilities/index.d.ts",
|
|
19
|
-
"default": "./dist/lib/
|
|
19
|
+
"default": "./dist/lib/capabilities.mjs"
|
|
20
20
|
},
|
|
21
21
|
"#components": {
|
|
22
22
|
"source": "./src/components/index.ts",
|
|
23
23
|
"types": "./dist/types/src/components/index.d.ts",
|
|
24
|
-
"default": "./dist/lib/
|
|
24
|
+
"default": "./dist/lib/components.mjs"
|
|
25
25
|
},
|
|
26
26
|
"#containers": {
|
|
27
27
|
"source": "./src/containers/index.ts",
|
|
28
28
|
"types": "./dist/types/src/containers/index.d.ts",
|
|
29
|
-
"default": "./dist/lib/
|
|
29
|
+
"default": "./dist/lib/containers.mjs"
|
|
30
30
|
},
|
|
31
31
|
"#hooks": {
|
|
32
32
|
"source": "./src/hooks/index.ts",
|
|
33
33
|
"types": "./dist/types/src/hooks/index.d.ts",
|
|
34
|
-
"default": "./dist/lib/
|
|
34
|
+
"default": "./dist/lib/hooks.mjs"
|
|
35
35
|
},
|
|
36
36
|
"#meta": {
|
|
37
37
|
"source": "./src/meta.ts",
|
|
38
38
|
"types": "./dist/types/src/meta.d.ts",
|
|
39
|
-
"default": "./dist/lib/
|
|
39
|
+
"default": "./dist/lib/meta.mjs"
|
|
40
40
|
},
|
|
41
41
|
"#operations": {
|
|
42
42
|
"source": "./src/operations/index.ts",
|
|
43
43
|
"types": "./dist/types/src/operations/index.d.ts",
|
|
44
|
-
"default": "./dist/lib/
|
|
44
|
+
"default": "./dist/lib/operations.mjs"
|
|
45
45
|
},
|
|
46
46
|
"#plugin": {
|
|
47
47
|
"source": "./src/SearchPlugin.tsx",
|
|
48
48
|
"types": "./dist/types/src/SearchPlugin.d.ts",
|
|
49
|
-
"default": "./dist/lib/
|
|
49
|
+
"default": "./dist/lib/SearchPlugin.mjs"
|
|
50
50
|
},
|
|
51
51
|
"#translations": {
|
|
52
52
|
"source": "./src/translations.ts",
|
|
53
53
|
"types": "./dist/types/src/translations.d.ts",
|
|
54
|
-
"default": "./dist/lib/
|
|
54
|
+
"default": "./dist/lib/translations.mjs"
|
|
55
55
|
},
|
|
56
56
|
"#types": {
|
|
57
57
|
"source": "./src/types/index.ts",
|
|
58
58
|
"types": "./dist/types/src/types/index.d.ts",
|
|
59
|
-
"default": "./dist/lib/
|
|
59
|
+
"default": "./dist/lib/types.mjs"
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"exports": {
|
|
63
63
|
".": {
|
|
64
64
|
"source": "./src/index.ts",
|
|
65
65
|
"types": "./dist/types/src/index.d.ts",
|
|
66
|
-
"
|
|
66
|
+
"import": "./dist/lib/index.mjs"
|
|
67
67
|
},
|
|
68
68
|
"./assets/PLUGIN.mdl": "./PLUGIN.mdl",
|
|
69
69
|
"./plugin": {
|
|
70
70
|
"source": "./src/plugin.ts",
|
|
71
71
|
"types": "./dist/types/src/plugin.d.ts",
|
|
72
|
-
"
|
|
72
|
+
"import": "./dist/lib/plugin.mjs"
|
|
73
73
|
},
|
|
74
74
|
"./testing": {
|
|
75
75
|
"source": "./src/testing.ts",
|
|
76
76
|
"types": "./dist/types/src/testing.d.ts",
|
|
77
|
-
"
|
|
77
|
+
"import": "./dist/lib/testing.mjs"
|
|
78
78
|
},
|
|
79
79
|
"./translations": {
|
|
80
80
|
"source": "./src/translations.ts",
|
|
81
81
|
"types": "./dist/types/src/translations.d.ts",
|
|
82
|
-
"
|
|
82
|
+
"import": "./dist/lib/translations.mjs"
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
"types": "dist/types/src/index.d.ts",
|
|
@@ -91,43 +91,46 @@
|
|
|
91
91
|
],
|
|
92
92
|
"dependencies": {
|
|
93
93
|
"main-thread-scheduling": "^14.1.1",
|
|
94
|
-
"@dxos/ai": "0.
|
|
95
|
-
"@dxos/app-framework": "0.
|
|
96
|
-
"@dxos/app-toolkit": "0.
|
|
97
|
-
"@dxos/
|
|
98
|
-
"@dxos/
|
|
99
|
-
"@dxos/
|
|
100
|
-
"@dxos/echo-protocol": "0.
|
|
101
|
-
"@dxos/
|
|
102
|
-
"@dxos/
|
|
103
|
-
"@dxos/
|
|
104
|
-
"@dxos/
|
|
105
|
-
"@dxos/
|
|
106
|
-
"@dxos/plugin-
|
|
107
|
-
"@dxos/
|
|
108
|
-
"@dxos/react-
|
|
109
|
-
"@dxos/react-ui-
|
|
110
|
-
"@dxos/react-ui-
|
|
111
|
-
"@dxos/react-ui-
|
|
112
|
-
"@dxos/
|
|
113
|
-
"@dxos/
|
|
114
|
-
"@dxos/
|
|
94
|
+
"@dxos/ai": "0.11.0",
|
|
95
|
+
"@dxos/app-framework": "0.11.0",
|
|
96
|
+
"@dxos/app-toolkit": "0.11.0",
|
|
97
|
+
"@dxos/compute": "0.11.0",
|
|
98
|
+
"@dxos/debug": "0.11.0",
|
|
99
|
+
"@dxos/echo": "0.11.0",
|
|
100
|
+
"@dxos/echo-protocol": "0.11.0",
|
|
101
|
+
"@dxos/echo-react": "0.11.0",
|
|
102
|
+
"@dxos/effect": "0.11.0",
|
|
103
|
+
"@dxos/invariant": "0.11.0",
|
|
104
|
+
"@dxos/keys": "0.11.0",
|
|
105
|
+
"@dxos/log": "0.11.0",
|
|
106
|
+
"@dxos/plugin-client": "0.11.0",
|
|
107
|
+
"@dxos/plugin-graph": "0.11.0",
|
|
108
|
+
"@dxos/react-client": "0.11.0",
|
|
109
|
+
"@dxos/react-ui-attention": "0.11.0",
|
|
110
|
+
"@dxos/react-ui-list": "0.11.0",
|
|
111
|
+
"@dxos/react-ui-menu": "0.11.0",
|
|
112
|
+
"@dxos/react-ui-mosaic": "0.11.0",
|
|
113
|
+
"@dxos/react-ui-search": "0.11.0",
|
|
114
|
+
"@dxos/schema": "0.11.0",
|
|
115
|
+
"@dxos/types": "0.11.0",
|
|
116
|
+
"@dxos/util": "0.11.0"
|
|
115
117
|
},
|
|
116
118
|
"devDependencies": {
|
|
117
119
|
"@effect-atom/atom-react": "^0.5.0",
|
|
118
120
|
"@effect/platform": "0.96.2",
|
|
121
|
+
"@testing-library/react": "^16.3.2",
|
|
119
122
|
"@types/react": "~19.2.17",
|
|
120
123
|
"@types/react-dom": "~19.2.3",
|
|
121
124
|
"effect": "3.21.4",
|
|
122
125
|
"react": "~19.2.7",
|
|
123
126
|
"react-dom": "~19.2.7",
|
|
124
|
-
"vite": "^8.
|
|
125
|
-
"@dxos/plugin-
|
|
126
|
-
"@dxos/plugin-
|
|
127
|
-
"@dxos/random": "0.
|
|
128
|
-
"@dxos/
|
|
129
|
-
"@dxos/
|
|
130
|
-
"@dxos/ui-theme": "0.
|
|
127
|
+
"vite": "^8.1.4",
|
|
128
|
+
"@dxos/plugin-preview": "0.11.0",
|
|
129
|
+
"@dxos/plugin-testing": "0.11.0",
|
|
130
|
+
"@dxos/random": "0.11.0",
|
|
131
|
+
"@dxos/react-ui": "0.11.0",
|
|
132
|
+
"@dxos/storybook-utils": "0.11.0",
|
|
133
|
+
"@dxos/ui-theme": "0.11.0"
|
|
131
134
|
},
|
|
132
135
|
"peerDependencies": {
|
|
133
136
|
"@effect-atom/atom-react": "^0.5.0",
|
|
@@ -135,8 +138,8 @@
|
|
|
135
138
|
"effect": "3.21.4",
|
|
136
139
|
"react": "~19.2.7",
|
|
137
140
|
"react-dom": "~19.2.7",
|
|
138
|
-
"@dxos/react-ui": "0.
|
|
139
|
-
"@dxos/ui-theme": "0.
|
|
141
|
+
"@dxos/react-ui": "^0.11.0",
|
|
142
|
+
"@dxos/ui-theme": "^0.11.0"
|
|
140
143
|
},
|
|
141
144
|
"publishConfig": {
|
|
142
145
|
"access": "public"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type ComponentProps } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useActiveSpace } from '@dxos/app-toolkit/ui';
|
|
8
|
+
import { type Space } from '@dxos/react-client/echo';
|
|
9
|
+
|
|
10
|
+
import { SearchArticle, SearchDialog } from '#containers';
|
|
11
|
+
import { SearchContextProvider } from '#hooks';
|
|
12
|
+
|
|
13
|
+
export type SearchDialogSurfaceProps = {
|
|
14
|
+
props: ComponentProps<typeof SearchDialog>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Search is scoped to the active space, so each surface resolves it before mounting the provider. */
|
|
18
|
+
export const SearchDialogSurface = ({ props }: SearchDialogSurfaceProps) => {
|
|
19
|
+
const space = useActiveSpace();
|
|
20
|
+
if (!space) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<SearchContextProvider>
|
|
26
|
+
<SearchDialog {...props} space={space} />
|
|
27
|
+
</SearchContextProvider>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const SearchInputSurface = () => {
|
|
32
|
+
const space = useActiveSpace();
|
|
33
|
+
if (!space) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<SearchContextProvider>
|
|
39
|
+
<SearchArticle space={space} />
|
|
40
|
+
</SearchContextProvider>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type SearchCompanionSurfaceProps = {
|
|
45
|
+
space: Space;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const SearchCompanionSurface = ({ space }: SearchCompanionSurfaceProps) => {
|
|
49
|
+
if (!space) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<SearchContextProvider>
|
|
55
|
+
<SearchArticle space={space} />
|
|
56
|
+
</SearchContextProvider>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
import * as Effect from 'effect/Effect';
|
|
6
6
|
|
|
7
7
|
import { Capability } from '@dxos/app-framework';
|
|
8
|
-
import { AppCapabilities, AppNode,
|
|
8
|
+
import { AppCapabilities, AppNode, GraphPath } from '@dxos/app-toolkit';
|
|
9
9
|
import { Operation } from '@dxos/compute';
|
|
10
10
|
import { ClientCapabilities } from '@dxos/plugin-client';
|
|
11
11
|
import { GraphBuilder, NodeMatcher } from '@dxos/plugin-graph';
|
|
12
|
-
import {
|
|
12
|
+
import { Attention } from '@dxos/react-ui-attention';
|
|
13
13
|
|
|
14
14
|
import { meta } from '#meta';
|
|
15
15
|
import { SearchOperation } from '#types';
|
|
@@ -25,12 +25,12 @@ export default Capability.makeModule(
|
|
|
25
25
|
const client = yield* Capability.get(ClientCapabilities.Client);
|
|
26
26
|
const layoutAtom = get(yield* Capability.atom(AppCapabilities.Layout))[0];
|
|
27
27
|
const layout = layoutAtom ? get(layoutAtom) : undefined;
|
|
28
|
-
const spaceId = layout?.workspace ?
|
|
28
|
+
const spaceId = layout?.workspace ? GraphPath.getSpaceIdFromPath(layout.workspace) : undefined;
|
|
29
29
|
const space = spaceId ? client.spaces.get(spaceId) : null;
|
|
30
30
|
|
|
31
31
|
return [
|
|
32
32
|
AppNode.makeDeckCompanion({
|
|
33
|
-
id: linkedSegment('search'),
|
|
33
|
+
id: Attention.linkedSegment('search'),
|
|
34
34
|
label: ['search.label', { ns: meta.profile.key }],
|
|
35
35
|
icon: 'ph--magnifying-glass--regular',
|
|
36
36
|
data: space,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import { type ComponentProps } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Capabilities, Capability } from '@dxos/app-framework';
|
|
9
|
+
import { Surface } from '@dxos/app-framework/ui';
|
|
10
|
+
import { AppSurface } from '@dxos/app-toolkit/ui';
|
|
11
|
+
import { isSpace } from '@dxos/react-client/echo';
|
|
12
|
+
|
|
13
|
+
import { type SearchDialog } from '#containers';
|
|
14
|
+
|
|
15
|
+
import { SEARCH_DIALOG } from '../constants';
|
|
16
|
+
import { SearchCompanionSurface, SearchDialogSurface, SearchInputSurface } from './SearchSurfaces';
|
|
17
|
+
|
|
18
|
+
export default Capability.makeModule(() =>
|
|
19
|
+
Effect.succeed(
|
|
20
|
+
Capability.contributes(Capabilities.ReactSurface, [
|
|
21
|
+
Surface.create({
|
|
22
|
+
id: SEARCH_DIALOG,
|
|
23
|
+
filter: AppSurface.component<ComponentProps<typeof SearchDialog>>(AppSurface.Dialog, SEARCH_DIALOG),
|
|
24
|
+
component: SearchDialogSurface,
|
|
25
|
+
props: ({ data: { props } }) => ({ props }),
|
|
26
|
+
}),
|
|
27
|
+
Surface.create({
|
|
28
|
+
id: `${SEARCH_DIALOG}.searchInput`,
|
|
29
|
+
filter: Surface.makeFilter(AppSurface.SearchInput),
|
|
30
|
+
component: SearchInputSurface,
|
|
31
|
+
}),
|
|
32
|
+
Surface.create({
|
|
33
|
+
id: `${SEARCH_DIALOG}.search`,
|
|
34
|
+
filter: AppSurface.subject(AppSurface.deckCompanion('search'), isSpace),
|
|
35
|
+
component: SearchCompanionSurface,
|
|
36
|
+
props: ({ data: { subject } }) => ({ space: subject }),
|
|
37
|
+
}),
|
|
38
|
+
]),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
8
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
9
|
+
|
|
10
|
+
import { Capability } from '@dxos/app-framework';
|
|
11
|
+
import { withPluginManager } from '@dxos/app-framework/testing';
|
|
12
|
+
import { AppCapabilities } from '@dxos/app-toolkit';
|
|
13
|
+
import { Obj } from '@dxos/echo';
|
|
14
|
+
import { useQuery } from '@dxos/echo-react';
|
|
15
|
+
import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
|
|
16
|
+
import { SAMPLE_MESSAGES, StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
|
|
17
|
+
import { random } from '@dxos/random';
|
|
18
|
+
import { useSpaces } from '@dxos/react-client/echo';
|
|
19
|
+
import { type SearchResult, buildSnippet } from '@dxos/react-ui-search';
|
|
20
|
+
import { Loading, withLayout } from '@dxos/react-ui/testing';
|
|
21
|
+
import { Message } from '@dxos/types';
|
|
22
|
+
|
|
23
|
+
import { buildSearchQuery, toSearchResults } from '#hooks';
|
|
24
|
+
import { translations } from '#translations';
|
|
25
|
+
|
|
26
|
+
import { SearchResultList } from './SearchResultList';
|
|
27
|
+
|
|
28
|
+
random.seed(0);
|
|
29
|
+
|
|
30
|
+
/** Real term repeated across several `SAMPLE_MESSAGES` entries. */
|
|
31
|
+
const SEARCH_TERM = 'invoice';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Enriches a raw `SearchResult` with a best-match snippet and the sender's name as metadata —
|
|
35
|
+
* `toSearchResults` produces a generic snippet, but a `Message`'s full text and sender give a
|
|
36
|
+
* better demo of the row layout.
|
|
37
|
+
*/
|
|
38
|
+
const enrichResult = (result: SearchResult, query: string): SearchResult => {
|
|
39
|
+
const message = result.object && Obj.instanceOf(Message.Message, result.object) ? result.object : undefined;
|
|
40
|
+
if (!message) {
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...result,
|
|
46
|
+
snippet: buildSnippet(Message.extractText(message), query),
|
|
47
|
+
type: message.sender.name ?? 'Message',
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const DefaultStory = () => {
|
|
52
|
+
const spaces = useSpaces();
|
|
53
|
+
const space = spaces[spaces.length - 1];
|
|
54
|
+
const [query, setQuery] = useState('');
|
|
55
|
+
|
|
56
|
+
const objects = useQuery(space?.db, buildSearchQuery(query));
|
|
57
|
+
const results = useMemo(() => (query ? toSearchResults(objects, query) : []), [objects, query]);
|
|
58
|
+
const enrichedResults = useMemo(() => results.map((result) => enrichResult(result, query)), [results, query]);
|
|
59
|
+
|
|
60
|
+
const handleSelect = useCallback((result: SearchResult) => {
|
|
61
|
+
// Storybook demo only — selection has no further destination.
|
|
62
|
+
console.log('select', result.id);
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
if (!space) {
|
|
66
|
+
return <Loading />;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className='flex flex-col gap-2 p-2 max-w-[32rem]'>
|
|
71
|
+
<input
|
|
72
|
+
type='text'
|
|
73
|
+
placeholder='Search…'
|
|
74
|
+
value={query}
|
|
75
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
76
|
+
className='px-2 py-1 border border-separator rounded'
|
|
77
|
+
/>
|
|
78
|
+
<SearchResultList results={enrichedResults} query={query} onSelect={handleSelect} />
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const meta = {
|
|
84
|
+
title: 'plugins/plugin-search/components/SearchResultList',
|
|
85
|
+
render: DefaultStory,
|
|
86
|
+
decorators: [
|
|
87
|
+
withLayout({ layout: 'column' }),
|
|
88
|
+
withPluginManager({
|
|
89
|
+
capabilities: [Capability.contributes(AppCapabilities.Translations, translations)],
|
|
90
|
+
plugins: [
|
|
91
|
+
...corePlugins(),
|
|
92
|
+
StorybookPlugin({}),
|
|
93
|
+
ClientPlugin({
|
|
94
|
+
types: [Message.Message],
|
|
95
|
+
onClientInitialized: ({ client }) =>
|
|
96
|
+
Effect.gen(function* () {
|
|
97
|
+
const { personalSpace } = yield* initializeIdentity(client);
|
|
98
|
+
|
|
99
|
+
for (const { from, subject, body } of SAMPLE_MESSAGES) {
|
|
100
|
+
personalSpace.db.add(
|
|
101
|
+
Message.make({
|
|
102
|
+
sender: { email: from.email, name: from.name },
|
|
103
|
+
blocks: [{ _tag: 'text', text: body }],
|
|
104
|
+
properties: { subject, snippet: body.slice(0, 120) },
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
yield* Effect.promise(() => personalSpace.db.flush({ indexes: true }));
|
|
109
|
+
}),
|
|
110
|
+
}),
|
|
111
|
+
],
|
|
112
|
+
}),
|
|
113
|
+
],
|
|
114
|
+
tags: ['test'],
|
|
115
|
+
parameters: {
|
|
116
|
+
layout: 'fullscreen',
|
|
117
|
+
},
|
|
118
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
119
|
+
|
|
120
|
+
export default meta;
|
|
121
|
+
|
|
122
|
+
type Story = StoryObj<typeof meta>;
|
|
123
|
+
|
|
124
|
+
export const Default: Story = {};
|
|
125
|
+
|
|
126
|
+
export const Test: Story = {
|
|
127
|
+
play: async ({ canvasElement }) => {
|
|
128
|
+
const canvas = within(canvasElement);
|
|
129
|
+
|
|
130
|
+
const input = await canvas.findByPlaceholderText('Search…', undefined, { timeout: 10_000 });
|
|
131
|
+
await userEvent.type(input, SEARCH_TERM);
|
|
132
|
+
|
|
133
|
+
// The seeded corpus repeats "invoice" across several finance messages, so this should surface
|
|
134
|
+
// a proper multi-row match set — the end-to-end proof that FTS + ranking + rendering are wired.
|
|
135
|
+
await waitFor(
|
|
136
|
+
async () => {
|
|
137
|
+
const rows = canvas.queryAllByRole('listitem');
|
|
138
|
+
await expect(rows.length).toBeGreaterThanOrEqual(2);
|
|
139
|
+
},
|
|
140
|
+
{ timeout: 15_000 },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const marks = canvasElement.querySelectorAll('mark');
|
|
144
|
+
const matchingMark = Array.from(marks).find((mark) => mark.textContent?.toLowerCase().includes(SEARCH_TERM));
|
|
145
|
+
await expect(matchingMark).toBeTruthy();
|
|
146
|
+
|
|
147
|
+
// Each row's metadata cell shows the sender's name (populated by `enrichResult`).
|
|
148
|
+
const rows = canvas.getAllByRole('listitem');
|
|
149
|
+
const metadataCells = rows.map((row) => row.querySelector('span.text-description'));
|
|
150
|
+
await expect(metadataCells.every((cell) => cell?.textContent)).toBe(true);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
6
|
+
import React, { type PropsWithChildren } from 'react';
|
|
7
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { ThemeProvider } from '@dxos/react-ui';
|
|
10
|
+
|
|
11
|
+
import { type SearchResult } from '#types';
|
|
12
|
+
|
|
13
|
+
import { SearchResultList } from './SearchResultList';
|
|
14
|
+
|
|
15
|
+
// `Listbox.Viewport` renders a `ScrollArea`, which reads theme tokens via context.
|
|
16
|
+
const Wrapper = ({ children }: PropsWithChildren) => <ThemeProvider>{children}</ThemeProvider>;
|
|
17
|
+
|
|
18
|
+
const results: SearchResult[] = [
|
|
19
|
+
{
|
|
20
|
+
id: '1',
|
|
21
|
+
label: 'Invoice #12345',
|
|
22
|
+
snippet: 'Please pay invoice #12345 by Friday.',
|
|
23
|
+
type: 'Message',
|
|
24
|
+
icon: 'ph--envelope--regular',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: '2',
|
|
28
|
+
label: 'Sprint planning notes',
|
|
29
|
+
snippet: 'Discussed the invoice reconciliation process for the sprint.',
|
|
30
|
+
type: 'Message',
|
|
31
|
+
icon: 'ph--envelope--regular',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
describe('SearchResultList', () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
cleanup();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('renders a row for each result', () => {
|
|
41
|
+
render(<SearchResultList results={results} query='invoice' />, { wrapper: Wrapper });
|
|
42
|
+
// `getByText` throws if no match is found, which is assertion enough that the row rendered.
|
|
43
|
+
expect(screen.getByText(/Sprint planning notes/)).toBeTruthy();
|
|
44
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(2);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('highlights the query term in both title and snippet', () => {
|
|
48
|
+
const { container } = render(<SearchResultList results={[results[0]]} query='invoice' />, { wrapper: Wrapper });
|
|
49
|
+
const marks = container.querySelectorAll('mark');
|
|
50
|
+
// One match in the title ("Invoice") and one in the snippet ("invoice").
|
|
51
|
+
expect(marks.length).toBeGreaterThanOrEqual(2);
|
|
52
|
+
expect(Array.from(marks).some((mark) => mark.textContent?.toLowerCase() === 'invoice')).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('renders metadata (type) alongside each result', () => {
|
|
56
|
+
render(<SearchResultList results={results} query='invoice' />, { wrapper: Wrapper });
|
|
57
|
+
expect(screen.getAllByText('Message')).toHaveLength(2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('renders no rows for an empty result set', () => {
|
|
61
|
+
render(<SearchResultList results={[]} query='invoice' />, { wrapper: Wrapper });
|
|
62
|
+
expect(screen.queryAllByRole('listitem')).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { useTranslation } from '@dxos/react-ui';
|
|
8
|
+
import { Empty, Listbox } from '@dxos/react-ui-list';
|
|
9
|
+
import { Highlighted, type SearchResult } from '@dxos/react-ui-search';
|
|
10
|
+
|
|
11
|
+
import { meta } from '#meta';
|
|
12
|
+
|
|
13
|
+
export type SearchResultListProps = {
|
|
14
|
+
/** Matched objects to render, already ranked. */
|
|
15
|
+
results: SearchResult[];
|
|
16
|
+
/** Current search query, used to highlight matches. */
|
|
17
|
+
query: string;
|
|
18
|
+
/** Called when a row is activated. */
|
|
19
|
+
onSelect?: (result: SearchResult) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dense, read-only search-results list: each row shows the matched object's icon, highlighted
|
|
24
|
+
* title and best-match snippet, and its type as trailing metadata. Built on `Listbox` in its
|
|
25
|
+
* plain (non-selectable) mode — rows are `role=listitem`, not `role=option`.
|
|
26
|
+
*/
|
|
27
|
+
export const SearchResultList = ({ results, query, onSelect }: SearchResultListProps) => {
|
|
28
|
+
const { t } = useTranslation(meta.profile.key);
|
|
29
|
+
return (
|
|
30
|
+
<Listbox.Root>
|
|
31
|
+
<Listbox.Viewport>
|
|
32
|
+
{results.length === 0 ? (
|
|
33
|
+
// `Empty` renders a `<div>`; keep it out of `Listbox.Content`'s `<ul>` rather than
|
|
34
|
+
// nesting a non-`<li>` child inside the list.
|
|
35
|
+
<Empty label={t('search-result-list.empty.label')} />
|
|
36
|
+
) : (
|
|
37
|
+
<Listbox.Content aria-label={t('search-result-list.label')}>
|
|
38
|
+
{results.map((result) => (
|
|
39
|
+
<Listbox.Item
|
|
40
|
+
key={result.id}
|
|
41
|
+
id={result.id}
|
|
42
|
+
classNames='grid grid-cols-[1fr_auto] items-center gap-2'
|
|
43
|
+
onClick={() => onSelect?.(result)}
|
|
44
|
+
>
|
|
45
|
+
<Listbox.ItemContent
|
|
46
|
+
icon={result.icon}
|
|
47
|
+
title={<Highlighted text={result.label ?? ''} query={query} />}
|
|
48
|
+
description={result.snippet ? <Highlighted text={result.snippet} query={query} /> : undefined}
|
|
49
|
+
/>
|
|
50
|
+
{result.type ? <span className='shrink-0 text-sm text-description'>{result.type}</span> : null}
|
|
51
|
+
</Listbox.Item>
|
|
52
|
+
))}
|
|
53
|
+
</Listbox.Content>
|
|
54
|
+
)}
|
|
55
|
+
</Listbox.Viewport>
|
|
56
|
+
</Listbox.Root>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
SearchResultList.displayName = 'SearchResultList';
|