@rpcbase/client 0.216.0 → 0.218.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/access-control/PolicyEditor/index.tsx +106 -11
- package/form/Input.tsx +2 -1
- package/hashState.js +1 -1
- package/package.json +1 -1
- package/ui/Search/SearchHistory/index.js +4 -0
- package/ui/Search/SearchHistory/useSearchHistory.tsx +2 -1
- package/ui/Search/SearchResults/index.tsx +90 -0
- package/ui/Search/index.tsx +15 -18
- package/ui/helpers/useActiveListItemIndex/index.tsx +4 -1
- package/ui/nav/ContentView/ContentViewContext.ts +1 -0
- package/ui/nav/ContentView/index.tsx +6 -3
- package/ui/nav/HeaderContainer/header.scss +0 -1
- package/ui/nav/HeaderContainer/index.tsx +7 -3
- package/ui/nav/SidebarContainer/sidebar-container.scss +0 -1
- package/ui/Search/SearchResults/getActions.js +0 -6
- package/ui/Search/SearchResults/index.js +0 -188
- package/ui/Search/SearchResults/useCombinedResultsActions.js +0 -59
|
@@ -1,16 +1,68 @@
|
|
|
1
1
|
import {useState} from "react"
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
const POLICY_EFFECT = [
|
|
5
|
+
{
|
|
6
|
+
name: "Grant",
|
|
7
|
+
key: "grant",
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: "Deny",
|
|
11
|
+
key: "deny",
|
|
12
|
+
},
|
|
13
|
+
]
|
|
14
|
+
|
|
4
15
|
const TARGET_TYPES = [
|
|
5
16
|
{
|
|
6
17
|
name: "Collection:",
|
|
7
|
-
key: "
|
|
8
|
-
description: "Applies to all documents within the collection
|
|
18
|
+
key: "collections",
|
|
19
|
+
description: "Applies to all documents within the collection",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "Documents:",
|
|
23
|
+
key: "documents",
|
|
24
|
+
description: "Applies only to certain specific documents",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "Field:",
|
|
28
|
+
key: "fields",
|
|
29
|
+
description: "Applies only specific fields of documents",
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const OPERATIONS = [
|
|
34
|
+
{
|
|
35
|
+
name: "Create",
|
|
36
|
+
key: "create",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "Read",
|
|
40
|
+
key: "read",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "Update",
|
|
44
|
+
key: "update",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "Delete",
|
|
48
|
+
key: "delete",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "All of the above",
|
|
52
|
+
key: "all",
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
//
|
|
58
|
+
const APPLIES_TO = [
|
|
59
|
+
{
|
|
60
|
+
name: "Users",
|
|
61
|
+
key: "users",
|
|
9
62
|
},
|
|
10
63
|
{
|
|
11
|
-
name: "
|
|
12
|
-
key: "
|
|
13
|
-
description: "Applies only to certain specific documents.",
|
|
64
|
+
name: "Groups",
|
|
65
|
+
key: "groups",
|
|
14
66
|
},
|
|
15
67
|
]
|
|
16
68
|
|
|
@@ -24,7 +76,31 @@ export const PolicyEditor = () => {
|
|
|
24
76
|
return (
|
|
25
77
|
<div>
|
|
26
78
|
<h6>Policy Editor</h6>
|
|
27
|
-
<
|
|
79
|
+
<hr />
|
|
80
|
+
|
|
81
|
+
<h6>Effect:</h6>
|
|
82
|
+
<div className="mb-3" style={{maxWidth: 300}}>
|
|
83
|
+
{POLICY_EFFECT.map((type) => (
|
|
84
|
+
<div key={type.key} className="d-flex flex-row mb-1">
|
|
85
|
+
<input
|
|
86
|
+
className="form-check-input"
|
|
87
|
+
type="radio"
|
|
88
|
+
name="targetTypeOptions"
|
|
89
|
+
id={type.key}
|
|
90
|
+
value={type.key}
|
|
91
|
+
checked={targetType === type.key}
|
|
92
|
+
onChange={onChangeTargetType}
|
|
93
|
+
/>
|
|
94
|
+
<label
|
|
95
|
+
className="form-check-label cursor-pointer ps-2"
|
|
96
|
+
htmlFor={type.key}
|
|
97
|
+
>
|
|
98
|
+
<b>{type.name}</b>
|
|
99
|
+
</label>
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
28
104
|
<div className="d-flex flex-row">
|
|
29
105
|
<div className="me-2">
|
|
30
106
|
<h6>Target Type:</h6>
|
|
@@ -58,11 +134,30 @@ export const PolicyEditor = () => {
|
|
|
58
134
|
</div>
|
|
59
135
|
</div>
|
|
60
136
|
</div>
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
137
|
+
|
|
138
|
+
<h6 className="mt-3">Operation:</h6>
|
|
139
|
+
<div className="" style={{maxWidth: 300}}>
|
|
140
|
+
{OPERATIONS.map((type) => (
|
|
141
|
+
<div key={type.key} className="d-flex flex-row mb-1">
|
|
142
|
+
<input
|
|
143
|
+
className="form-check-input"
|
|
144
|
+
type="radio"
|
|
145
|
+
name="targetTypeOptions"
|
|
146
|
+
id={type.key}
|
|
147
|
+
value={type.key}
|
|
148
|
+
checked={targetType === type.key}
|
|
149
|
+
onChange={onChangeTargetType}
|
|
150
|
+
/>
|
|
151
|
+
<label
|
|
152
|
+
className="form-check-label cursor-pointer ps-2"
|
|
153
|
+
htmlFor={type.key}
|
|
154
|
+
>
|
|
155
|
+
<b>{type.name}</b>
|
|
156
|
+
</label>
|
|
157
|
+
</div>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
66
161
|
<br />
|
|
67
162
|
to attributes / conditions add support for expiry date
|
|
68
163
|
</div>
|
package/form/Input.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
// import {NestedKeyOf} from "@rpcbase/client/types"
|
|
3
|
+
import _snakeCase from "lodash/snakeCase"
|
|
3
4
|
import _get from "lodash/get"
|
|
4
5
|
import {useFormContext} from "./hook-form"
|
|
5
6
|
|
|
@@ -33,7 +34,7 @@ export const Input = <T,>({
|
|
|
33
34
|
}: InputProps<T> & React.InputHTMLAttributes<HTMLInputElement>) => {
|
|
34
35
|
const form = useFormContext()
|
|
35
36
|
|
|
36
|
-
const id = idProp || `
|
|
37
|
+
const id = idProp || _snakeCase(`input_${field}`)
|
|
37
38
|
|
|
38
39
|
const error = _get(form.formState.errors, field)
|
|
39
40
|
|
package/hashState.js
CHANGED
|
@@ -108,7 +108,7 @@ export const HashStateProvider = ({children}) => {
|
|
|
108
108
|
|
|
109
109
|
useEffect(() => {
|
|
110
110
|
apply_hash_state(hashState)
|
|
111
|
-
window.
|
|
111
|
+
window.__PRIVATE_HASH_STATE_DO_NOT_USE__ = hashState
|
|
112
112
|
}, [hashState])
|
|
113
113
|
|
|
114
114
|
const serializeHashState = useCallback((payload) => {
|
package/package.json
CHANGED
|
@@ -6,13 +6,14 @@ import {getUid} from "@rpcbase/client/auth/getUid"
|
|
|
6
6
|
|
|
7
7
|
import stripDiacritics from "react-bootstrap-typeahead/cjs/utils/stripDiacritics"
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
const HISTORY_MAX_RESULTS = 10
|
|
10
11
|
|
|
11
12
|
export const useSearchHistory = () => {
|
|
12
13
|
const user_id = getUid()
|
|
13
14
|
|
|
14
15
|
const [history, setHistory] = useState([])
|
|
15
|
-
const historyQuery = useQuery("
|
|
16
|
+
const historyQuery = useQuery("SearchHistory", {_owners: {$in: [user_id]}})
|
|
16
17
|
|
|
17
18
|
// Initial load history
|
|
18
19
|
useEffect(() => {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import _pick from "lodash/pick"
|
|
2
|
+
import _snakeCase from "lodash/snakeCase"
|
|
3
|
+
|
|
4
|
+
import {useHashState} from "../../../hashState"
|
|
5
|
+
|
|
6
|
+
import {useActiveListItemIndex} from "../../helpers/useActiveListItemIndex"
|
|
7
|
+
import {useScrollSelectorIntoView} from "../../helpers/useScrollSelectorIntoView"
|
|
8
|
+
|
|
9
|
+
// import save_search from "rpc!server/search-indexer/save_search"
|
|
10
|
+
|
|
11
|
+
export const SearchResults = ({
|
|
12
|
+
id = "search-results",
|
|
13
|
+
query,
|
|
14
|
+
results,
|
|
15
|
+
searchContext,
|
|
16
|
+
onCompleteSearch,
|
|
17
|
+
children,
|
|
18
|
+
renderResultItem,
|
|
19
|
+
}) => {
|
|
20
|
+
const {serializeHashState} = useHashState()
|
|
21
|
+
|
|
22
|
+
const getItemId = (i) => `${id}-result-${i}`
|
|
23
|
+
|
|
24
|
+
const getLink = (item) =>{
|
|
25
|
+
return `#${_snakeCase(item)}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const onSelectItem = (selectedIndex) => {
|
|
29
|
+
// console.log("ON SELEC", selectedIndex)
|
|
30
|
+
serializeHashState({link: getLink(results[selectedIndex])})
|
|
31
|
+
// if (window)
|
|
32
|
+
|
|
33
|
+
onCompleteSearch()
|
|
34
|
+
// go to selected item
|
|
35
|
+
// const targetItem = items[selectedIndex]
|
|
36
|
+
// applyTargetItem(targetItem)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const {activeItemIndex} = useActiveListItemIndex({items: results, onSelectItem})
|
|
40
|
+
|
|
41
|
+
useScrollSelectorIntoView(`#${getItemId(activeItemIndex)}`)
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
<div className="list-group list-group-flush">
|
|
46
|
+
{results.map((item: any, index: number) => {
|
|
47
|
+
|
|
48
|
+
const key = getItemId(index)
|
|
49
|
+
|
|
50
|
+
const onClick = () => {
|
|
51
|
+
onSelectItem(index)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<a
|
|
56
|
+
id={key}
|
|
57
|
+
key={key}
|
|
58
|
+
className={cx(
|
|
59
|
+
"list-group-item list-group-item-action d-flex justify-content-between align-items-start",
|
|
60
|
+
{active: activeItemIndex === index},
|
|
61
|
+
)}
|
|
62
|
+
href={getLink(item)}
|
|
63
|
+
onClick={onClick}
|
|
64
|
+
>
|
|
65
|
+
<div className="ms-2 me-auto">
|
|
66
|
+
{renderResultItem(item, index)}
|
|
67
|
+
{/* {Object.keys(item.highlight).map((highlightKey, j) => (
|
|
68
|
+
<div
|
|
69
|
+
key={`highlight-${j}`}
|
|
70
|
+
className="item-highlight"
|
|
71
|
+
dangerouslySetInnerHTML={{__html: item.highlight[highlightKey][0]}}
|
|
72
|
+
/>
|
|
73
|
+
))} */}
|
|
74
|
+
<small className="fst-italic">{"displaySub"}</small>
|
|
75
|
+
</div>
|
|
76
|
+
{/* <span className="badge text-bg-light rounded-pill">
|
|
77
|
+
{item.fuzzy_score?.toFixed(2)}
|
|
78
|
+
</span> */}
|
|
79
|
+
{/* <span className="badge bg-primary rounded-pill">{item._score.toFixed(2)}</span> */}
|
|
80
|
+
</a>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
})}
|
|
84
|
+
|
|
85
|
+
{/* children can be components like search history, etc */}
|
|
86
|
+
{children}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
package/ui/Search/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "assert"
|
|
2
|
-
import {useEffect, useState,
|
|
2
|
+
import {ReactNode, useEffect, useState, useRef, useCallback} from "react"
|
|
3
3
|
import {Typeahead} from "react-bootstrap-typeahead"
|
|
4
4
|
import Overlay from "react-bootstrap/Overlay"
|
|
5
5
|
import _throttle from "lodash/throttle"
|
|
@@ -27,19 +27,22 @@ const MIN_SEARCH_LENGTH = 2
|
|
|
27
27
|
const PLACEHOLDER = "Search..."
|
|
28
28
|
|
|
29
29
|
export const Search = ({
|
|
30
|
+
id = "search-input",
|
|
30
31
|
minLength = MIN_SEARCH_LENGTH,
|
|
31
32
|
onSearch,
|
|
33
|
+
renderResultItem,
|
|
32
34
|
}: {
|
|
35
|
+
id?: string,
|
|
33
36
|
minLength?: number
|
|
34
|
-
onSearch: (queryStr: string) => Array<any
|
|
37
|
+
onSearch: (queryStr: string) => Promise<Array<any>>
|
|
38
|
+
renderResultItem: (item: any, index: number) => ReactNode
|
|
35
39
|
}) => {
|
|
36
40
|
// const envContext = useEnvContext()
|
|
37
41
|
// const itemContext = useContext(ItemContext)
|
|
38
|
-
|
|
39
42
|
const typeaheadRef = useRef()
|
|
40
43
|
const wrapperRef = useRef()
|
|
41
44
|
|
|
42
|
-
const [results, setResults] = useState([])
|
|
45
|
+
const [results, setResults] = useState<Array<any>>([])
|
|
43
46
|
const [isLoading, setIsLoading] = useState(false)
|
|
44
47
|
const [isFocused, setIsFocused] = useState(false)
|
|
45
48
|
const [showResults, setShowResults] = useState(false)
|
|
@@ -116,17 +119,9 @@ export const Search = ({
|
|
|
116
119
|
setIsLoading(true)
|
|
117
120
|
}, 100)
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
// group_id: envContext.groupId,
|
|
121
|
-
// str,
|
|
122
|
-
// })
|
|
123
|
-
|
|
124
|
-
const results = await onSearch(queryStr)
|
|
125
|
-
|
|
126
|
-
// assert(res.status === "ok")
|
|
127
|
-
// console.log("search res", res)
|
|
122
|
+
const searchResults = await onSearch(queryStr)
|
|
128
123
|
|
|
129
|
-
setResults(
|
|
124
|
+
setResults(searchResults)
|
|
130
125
|
clearTimeout(tm)
|
|
131
126
|
setIsLoading(false)
|
|
132
127
|
},
|
|
@@ -216,16 +211,16 @@ export const Search = ({
|
|
|
216
211
|
return (
|
|
217
212
|
<>
|
|
218
213
|
<div
|
|
219
|
-
id=
|
|
214
|
+
id={id}
|
|
220
215
|
ref={wrapperRef}
|
|
221
216
|
className="d-flex mx-auto w-100"
|
|
222
217
|
// style={{height: 32, position: "relative", maxWidth: 804}}
|
|
223
218
|
style={{height: 32, position: "relative"}}
|
|
224
219
|
>
|
|
225
220
|
<Typeahead
|
|
226
|
-
id=
|
|
221
|
+
id={`${id}-typeahead`}
|
|
227
222
|
ref={typeaheadRef}
|
|
228
|
-
className={cx("search-
|
|
223
|
+
className={cx("search-typeahead w-100", {"is-focused": isFocused})}
|
|
229
224
|
placeholder={PLACEHOLDER}
|
|
230
225
|
inputProps={{
|
|
231
226
|
placeholder: PLACEHOLDER,
|
|
@@ -275,7 +270,7 @@ export const Search = ({
|
|
|
275
270
|
>
|
|
276
271
|
{({style, ...overlayProps}) => (
|
|
277
272
|
<div
|
|
278
|
-
id=
|
|
273
|
+
id={`${id}-overlay`}
|
|
279
274
|
{...overlayProps}
|
|
280
275
|
className="shadow-lg"
|
|
281
276
|
style={{
|
|
@@ -286,10 +281,12 @@ export const Search = ({
|
|
|
286
281
|
}}
|
|
287
282
|
>
|
|
288
283
|
<SearchResults
|
|
284
|
+
id={id}
|
|
289
285
|
query={currentInputValue.trim()}
|
|
290
286
|
results={results}
|
|
291
287
|
onCompleteSearch={onCompleteSearch}
|
|
292
288
|
searchContext={selected[0]}
|
|
289
|
+
renderResultItem={renderResultItem}
|
|
293
290
|
>
|
|
294
291
|
<SearchHistory setSearchInput={setSearchInput} />
|
|
295
292
|
</SearchResults>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import {useState, useEffect} from "react"
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
export const useActiveListItemIndex = ({items, activeItem, onSelectItem}) => {
|
|
5
|
+
|
|
4
6
|
const [activeItemIndex, setActiveItemIndex] = useState(() => {
|
|
5
7
|
let initialIndex = items.findIndex((item) => item.obj?._id === activeItem?._id)
|
|
6
|
-
if (
|
|
8
|
+
if (initialIndex < 0) initialIndex = 0
|
|
9
|
+
console.log("INITIAL INDEX", initialIndex)
|
|
7
10
|
return initialIndex
|
|
8
11
|
})
|
|
9
12
|
|
|
@@ -13,9 +13,11 @@ const SIDEVIEW_WIDTH = "sideview_width"
|
|
|
13
13
|
|
|
14
14
|
export const ContentView = memo(
|
|
15
15
|
({
|
|
16
|
+
headerHeight = 45,
|
|
16
17
|
children,
|
|
17
18
|
storageKeyPrefix: _storageKeyPrefix,
|
|
18
19
|
}: {
|
|
20
|
+
headerHeight?: number;
|
|
19
21
|
children: ReactNode;
|
|
20
22
|
storageKeyPrefix?: string;
|
|
21
23
|
}) => {
|
|
@@ -76,6 +78,7 @@ export const ContentView = memo(
|
|
|
76
78
|
contentViewHeight,
|
|
77
79
|
sidebarWidth,
|
|
78
80
|
setSidebarWidth,
|
|
81
|
+
headerHeight,
|
|
79
82
|
}}
|
|
80
83
|
>
|
|
81
84
|
<div
|
|
@@ -85,15 +88,15 @@ export const ContentView = memo(
|
|
|
85
88
|
flexDirection: "row",
|
|
86
89
|
}}
|
|
87
90
|
>
|
|
88
|
-
<div id="sidebar-container" />
|
|
91
|
+
<div id="sidebar-container" style={{top: headerHeight}} />
|
|
89
92
|
|
|
90
93
|
<div
|
|
91
94
|
ref={contentViewRef}
|
|
92
95
|
key={`content-wrapper-${"content_view_"}`}
|
|
93
96
|
style={{
|
|
94
97
|
width: `calc(100vw - ${contentWidthOffset}px)`,
|
|
95
|
-
height:
|
|
96
|
-
marginTop:
|
|
98
|
+
height: `calc(100vh - ${headerHeight}px)`,
|
|
99
|
+
marginTop: headerHeight,
|
|
97
100
|
overflowY: "scroll",
|
|
98
101
|
marginLeft: sidebarWidth,
|
|
99
102
|
}}
|
|
@@ -28,7 +28,7 @@ import {ReactNode} from "react"
|
|
|
28
28
|
// import EnvSettingsDropdown from "./components/EnvSettingsDropdown"
|
|
29
29
|
|
|
30
30
|
import "./header.scss"
|
|
31
|
-
|
|
31
|
+
import {useContentViewContext} from "../ContentView/ContentViewContext"
|
|
32
32
|
|
|
33
33
|
// const DefaultHeader = () => {
|
|
34
34
|
// return (
|
|
@@ -136,14 +136,18 @@ import "./header.scss"
|
|
|
136
136
|
// )
|
|
137
137
|
// }
|
|
138
138
|
|
|
139
|
-
export const HeaderContainer = ({children}: {children: ReactNode}) => {
|
|
139
|
+
export const HeaderContainer = ({children}: { children: ReactNode }) => {
|
|
140
|
+
const contentViewContext = useContentViewContext()
|
|
140
141
|
|
|
141
142
|
if (!children) return null
|
|
142
143
|
|
|
143
144
|
return (
|
|
144
145
|
<nav
|
|
145
146
|
id="header-container"
|
|
146
|
-
className={
|
|
147
|
+
className={
|
|
148
|
+
"d-flex align-items-center fixed-top bg-dark flex-md-nowrap shadow p-0 text-light"
|
|
149
|
+
}
|
|
150
|
+
style={{height: contentViewContext.headerHeight}}
|
|
147
151
|
>
|
|
148
152
|
{children}
|
|
149
153
|
</nav>
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import assert from "assert"
|
|
2
|
-
import _pick from "lodash/pick"
|
|
3
|
-
import fuzzysort from "fuzzysort"
|
|
4
|
-
|
|
5
|
-
import page from "../../../page"
|
|
6
|
-
// import {withHashState} from "@rpcbase/client/hashState"
|
|
7
|
-
|
|
8
|
-
// import COLLECTIONS from "config/collections"
|
|
9
|
-
// import {useEnvContext} from "helpers/EnvContext"
|
|
10
|
-
import {useActiveListItemIndex} from "../../helpers/useActiveListItemIndex"
|
|
11
|
-
import {useScrollSelectorIntoView} from "../../helpers/useScrollSelectorIntoView"
|
|
12
|
-
|
|
13
|
-
import useCombinedResultsActions from "./useCombinedResultsActions"
|
|
14
|
-
|
|
15
|
-
// import save_search from "rpc!server/search-indexer/save_search"
|
|
16
|
-
const save_search = async() => null
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const resolveTargetUrl = (encodeHashLink, item) => {
|
|
20
|
-
let targetUrl
|
|
21
|
-
|
|
22
|
-
// Message
|
|
23
|
-
if (item._source.type === "message") {
|
|
24
|
-
const pageUrl = "/channel"
|
|
25
|
-
const channelId = item._source.channel_id
|
|
26
|
-
const convId = item._source.conversation_id
|
|
27
|
-
targetUrl = `${pageUrl}/${channelId}#${encodeHashLink({
|
|
28
|
-
activeConversationId: convId, // conversation
|
|
29
|
-
scrollToMessageId: item._id, // highlighted message
|
|
30
|
-
})}`
|
|
31
|
-
}
|
|
32
|
-
// Other items
|
|
33
|
-
else {
|
|
34
|
-
const pageUrl = COLLECTIONS[item.meta.collection_name].base_url
|
|
35
|
-
targetUrl = `${pageUrl}/${item.meta._id}#${encodeHashLink({
|
|
36
|
-
highlightId: item._id,
|
|
37
|
-
})}`
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return targetUrl
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const getKey = (i) => `search-anything-item-${i}`
|
|
44
|
-
|
|
45
|
-
export const SearchResults = ({
|
|
46
|
-
query,
|
|
47
|
-
results,
|
|
48
|
-
searchContext,
|
|
49
|
-
onCompleteSearch,
|
|
50
|
-
children,
|
|
51
|
-
}) => {
|
|
52
|
-
// const envContext = useEnvContext()
|
|
53
|
-
|
|
54
|
-
const items = useCombinedResultsActions({query, results})
|
|
55
|
-
|
|
56
|
-
// runs when a search result is selected by pressing enter
|
|
57
|
-
const applyTargetItem = async(item) => {
|
|
58
|
-
// user pressed enter with no results / nothing selected
|
|
59
|
-
if (!item) {
|
|
60
|
-
console.log("user pressed enter but there are no results")
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (item.type === "action") {
|
|
65
|
-
if (item.hashState) {
|
|
66
|
-
serializeHashState(item.hashState)
|
|
67
|
-
} else {
|
|
68
|
-
console.error("unknown action", item)
|
|
69
|
-
throw new Error("unknown action")
|
|
70
|
-
}
|
|
71
|
-
} else {
|
|
72
|
-
const targetUrl = resolveTargetUrl(encodeHashLink, item)
|
|
73
|
-
|
|
74
|
-
const res = await save_search({
|
|
75
|
-
group_id: envContext.groupId,
|
|
76
|
-
data: {
|
|
77
|
-
query,
|
|
78
|
-
search_context: _pick(searchContext, ["id", "col"]),
|
|
79
|
-
result_url: targetUrl,
|
|
80
|
-
},
|
|
81
|
-
})
|
|
82
|
-
assert(res.status === "ok")
|
|
83
|
-
|
|
84
|
-
page(targetUrl)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const onSelectItem = (selectedIndex) => {
|
|
89
|
-
onCompleteSearch()
|
|
90
|
-
// go to selected item
|
|
91
|
-
const targetItem = items[selectedIndex]
|
|
92
|
-
applyTargetItem(targetItem)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const {activeItemIndex} = useActiveListItemIndex({items, onSelectItem})
|
|
96
|
-
|
|
97
|
-
useScrollSelectorIntoView(`#${getKey(activeItemIndex)}`)
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<div>
|
|
101
|
-
<div className="list-group list-group-flush">
|
|
102
|
-
{items.map((item, i) => {
|
|
103
|
-
const key = getKey(i)
|
|
104
|
-
// Render action
|
|
105
|
-
if (item.type === "action") {
|
|
106
|
-
let highlighted = fuzzysort.highlight(fuzzysort.single(query, item.key), "<b>", "</b>")
|
|
107
|
-
|
|
108
|
-
if (!highlighted) highlighted = item.key
|
|
109
|
-
|
|
110
|
-
const onClick = () => {
|
|
111
|
-
onSelectItem(i)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<a
|
|
116
|
-
id={key}
|
|
117
|
-
key={key}
|
|
118
|
-
className={cx(
|
|
119
|
-
"list-group-item list-group-item-action d-flex justify-content-between align-items-start",
|
|
120
|
-
{active: activeItemIndex === i},
|
|
121
|
-
)}
|
|
122
|
-
style={{cursor: "pointer"}}
|
|
123
|
-
onClick={onClick}
|
|
124
|
-
>
|
|
125
|
-
<div className="ms-2 me-auto">
|
|
126
|
-
<img
|
|
127
|
-
style={{width: 20, height: 20, display: "inline"}}
|
|
128
|
-
className="d-inline-flex me-2"
|
|
129
|
-
src={`/static/icons/${item.icon}.svg`}
|
|
130
|
-
/>
|
|
131
|
-
<div
|
|
132
|
-
className="item-highlight"
|
|
133
|
-
style={{display: "inline"}}
|
|
134
|
-
dangerouslySetInnerHTML={{__html: highlighted}}
|
|
135
|
-
/>
|
|
136
|
-
</div>
|
|
137
|
-
<span className="badge text-bg-light rounded-pill">
|
|
138
|
-
{item.fuzzy_score?.toFixed(2)}
|
|
139
|
-
</span>
|
|
140
|
-
</a>
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
// Render search result
|
|
144
|
-
else {
|
|
145
|
-
const onClick = () => {
|
|
146
|
-
onCompleteSearch()
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const targetUrl = resolveTargetUrl(encodeHashLink, item)
|
|
150
|
-
// TODO: when message, displaySub should be the name of the recipient / sender
|
|
151
|
-
const displaySub = item._source.type === "message" ? "message" : item.meta.name
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<a
|
|
155
|
-
id={key}
|
|
156
|
-
key={key}
|
|
157
|
-
className={cx(
|
|
158
|
-
"list-group-item list-group-item-action d-flex justify-content-between align-items-start",
|
|
159
|
-
{active: activeItemIndex === i},
|
|
160
|
-
)}
|
|
161
|
-
href={targetUrl}
|
|
162
|
-
onClick={onClick}
|
|
163
|
-
>
|
|
164
|
-
<div className="ms-2 me-auto">
|
|
165
|
-
{Object.keys(item.highlight).map((highlightKey, j) => (
|
|
166
|
-
<div
|
|
167
|
-
key={`highlight-${j}`}
|
|
168
|
-
className="item-highlight"
|
|
169
|
-
dangerouslySetInnerHTML={{__html: item.highlight[highlightKey][0]}}
|
|
170
|
-
/>
|
|
171
|
-
))}
|
|
172
|
-
<small className="fst-italic">{displaySub}</small>
|
|
173
|
-
</div>
|
|
174
|
-
<span className="badge text-bg-light rounded-pill">
|
|
175
|
-
{item.fuzzy_score?.toFixed(2)}
|
|
176
|
-
</span>
|
|
177
|
-
<span className="badge bg-primary rounded-pill">{item._score.toFixed(2)}</span>
|
|
178
|
-
</a>
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
})}
|
|
182
|
-
|
|
183
|
-
{/* children can be components like search history, etc */}
|
|
184
|
-
{children}
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
)
|
|
188
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/* @flow */
|
|
2
|
-
import {useMemo} from "react"
|
|
3
|
-
import useLocation from "react-use/lib/useLocation"
|
|
4
|
-
import _isNil from "lodash/isNil"
|
|
5
|
-
import fuzzysort from "fuzzysort"
|
|
6
|
-
|
|
7
|
-
import getActions from "./getActions"
|
|
8
|
-
|
|
9
|
-
// uses fuzzysort to match and sort results https://github.com/farzher/fuzzysort
|
|
10
|
-
const getResultsWithActions = (query, results, actions) => {
|
|
11
|
-
// we "fuzzy match sort" all actions and results (with both our fuzzy + elastic key)
|
|
12
|
-
const rawItems = [...actions, ...results].map((item) => {
|
|
13
|
-
let fuzzy_score
|
|
14
|
-
|
|
15
|
-
// action item
|
|
16
|
-
if (item.key) {
|
|
17
|
-
fuzzy_score = fuzzysort.single(query, item.key)?.score
|
|
18
|
-
}
|
|
19
|
-
// search result
|
|
20
|
-
else {
|
|
21
|
-
// for each highlight compute score, take the highest one
|
|
22
|
-
const scores = Object
|
|
23
|
-
// TODO: why do some elastic search results not have a highlight?
|
|
24
|
-
.keys(item?.highlight || {})
|
|
25
|
-
.map((key) => fuzzysort.single(query, item.highlight[key][0])?.score)
|
|
26
|
-
.filter((s) => !_isNil(s))
|
|
27
|
-
.sort((a, b) => b - a)
|
|
28
|
-
|
|
29
|
-
fuzzy_score = scores[0]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
...item,
|
|
34
|
-
fuzzy_score,
|
|
35
|
-
}
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const sortedItems = rawItems
|
|
39
|
-
.filter((item) => !_isNil(item.fuzzy_score))
|
|
40
|
-
.sort((a, b) => b.fuzzy_score - a.fuzzy_score)
|
|
41
|
-
|
|
42
|
-
return sortedItems
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const useCombinedResultsActions = ({query, results}) => {
|
|
46
|
-
const location = useLocation()
|
|
47
|
-
|
|
48
|
-
const items = useMemo(() => {
|
|
49
|
-
const actions = getActions(location.pathname)
|
|
50
|
-
|
|
51
|
-
const newItems = getResultsWithActions(query, results, actions)
|
|
52
|
-
|
|
53
|
-
return newItems
|
|
54
|
-
}, [results, location.pathname])
|
|
55
|
-
|
|
56
|
-
return items
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export default useCombinedResultsActions
|