@rpcbase/client 0.215.0 → 0.217.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/TargetSelector/QueryBuilder.tsx +48 -0
- package/access-control/PolicyEditor/TargetSelector/index.tsx +5 -0
- package/access-control/PolicyEditor/TargetSelector/query-builder.scss +9 -0
- package/access-control/PolicyEditor/index.tsx +70 -0
- package/access-control/index.ts +1 -0
- 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 +91 -0
- package/ui/Search/index.tsx +15 -18
- package/ui/SelectPills/index.tsx +20 -16
- package/ui/helpers/useActiveListItemIndex/index.tsx +4 -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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
QueryBuilderBootstrap,
|
|
3
|
+
bootstrapControlClassnames,
|
|
4
|
+
bootstrapControlElements,
|
|
5
|
+
} from "@react-querybuilder/bootstrap"
|
|
6
|
+
import {QueryBuilder as ReactQueryBuilder} from "react-querybuilder"
|
|
7
|
+
import {QueryBuilderDnD as ReactQueryBuilderDnD} from "@react-querybuilder/dnd"
|
|
8
|
+
import * as ReactDnD from "react-dnd"
|
|
9
|
+
import * as ReactDnDHtml5Backend from "react-dnd-html5-backend"
|
|
10
|
+
|
|
11
|
+
// import FieldSelector from "./FieldSelector"
|
|
12
|
+
// import OperatorSelector from "./OperatorSelector"
|
|
13
|
+
// import ValueEditor from "./ValueEditor"
|
|
14
|
+
|
|
15
|
+
import "./query-builder.scss"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const controlClassnames = {...bootstrapControlClassnames}
|
|
19
|
+
controlClassnames.queryBuilder += " queryBuilder-branches"
|
|
20
|
+
|
|
21
|
+
const QueryBuilder = ({controlElements = {}, ...props}) => {
|
|
22
|
+
return (
|
|
23
|
+
<ReactQueryBuilderDnD dnd={{...ReactDnD, ...ReactDnDHtml5Backend}}>
|
|
24
|
+
<QueryBuilderBootstrap>
|
|
25
|
+
<ReactQueryBuilder
|
|
26
|
+
addRuleToNewGroups
|
|
27
|
+
// TODO: why is DnD not working??
|
|
28
|
+
// https://react-querybuilder.js.org/docs/components/querybuilder#enabledraganddrop
|
|
29
|
+
enableDragAndDrop
|
|
30
|
+
showCombinatorsBetweenRules={false}
|
|
31
|
+
showNotToggle
|
|
32
|
+
showCloneButtons
|
|
33
|
+
controlClassnames={controlClassnames}
|
|
34
|
+
controlElements={{
|
|
35
|
+
...bootstrapControlElements,
|
|
36
|
+
...controlElements,
|
|
37
|
+
// fieldSelector: FieldSelector,
|
|
38
|
+
// operatorSelector: OperatorSelector,
|
|
39
|
+
// valueEditor: ValueEditor,
|
|
40
|
+
}}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
</QueryBuilderBootstrap>
|
|
44
|
+
</ReactQueryBuilderDnD>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default QueryBuilder
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {useState} from "react"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const TARGET_TYPES = [
|
|
5
|
+
{
|
|
6
|
+
name: "Collection:",
|
|
7
|
+
key: "collection",
|
|
8
|
+
description: "Applies to all documents within the collection.",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "Document:",
|
|
12
|
+
key: "document",
|
|
13
|
+
description: "Applies only to certain specific documents.",
|
|
14
|
+
},
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export const PolicyEditor = () => {
|
|
18
|
+
const [targetType, setTargetType] = useState("collection")
|
|
19
|
+
|
|
20
|
+
const onChangeTargetType = (event) => {
|
|
21
|
+
setTargetType(event.target.value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<h6>Policy Editor</h6>
|
|
27
|
+
<br />
|
|
28
|
+
<div className="d-flex flex-row">
|
|
29
|
+
<div className="me-2">
|
|
30
|
+
<h6>Target Type:</h6>
|
|
31
|
+
<div style={{maxWidth: 300}}>
|
|
32
|
+
{TARGET_TYPES.map((type) => (
|
|
33
|
+
<div key={type.key} className="d-flex flex-row mb-1">
|
|
34
|
+
<input
|
|
35
|
+
className="form-check-input"
|
|
36
|
+
type="radio"
|
|
37
|
+
name="targetTypeOptions"
|
|
38
|
+
id={type.key}
|
|
39
|
+
value={type.key}
|
|
40
|
+
checked={targetType === type.key}
|
|
41
|
+
onChange={onChangeTargetType}
|
|
42
|
+
/>
|
|
43
|
+
<label
|
|
44
|
+
className="form-check-label cursor-pointer ps-2"
|
|
45
|
+
htmlFor={type.key}
|
|
46
|
+
>
|
|
47
|
+
<b>{type.name}</b> {type.description}
|
|
48
|
+
</label>
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div>
|
|
55
|
+
<h6>Targets:</h6>
|
|
56
|
+
<div>
|
|
57
|
+
<input type="text" />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
grant / deny
|
|
62
|
+
<br />
|
|
63
|
+
Scope: document, field
|
|
64
|
+
<br />
|
|
65
|
+
operation: create read write delete
|
|
66
|
+
<br />
|
|
67
|
+
to attributes / conditions add support for expiry date
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
package/access-control/index.ts
CHANGED
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,91 @@
|
|
|
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
|
+
console.log("CLICK item", item)
|
|
52
|
+
onSelectItem(index)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<a
|
|
57
|
+
id={key}
|
|
58
|
+
key={key}
|
|
59
|
+
className={cx(
|
|
60
|
+
"list-group-item list-group-item-action d-flex justify-content-between align-items-start",
|
|
61
|
+
{active: activeItemIndex === index},
|
|
62
|
+
)}
|
|
63
|
+
href={getLink(item)}
|
|
64
|
+
onClick={onClick}
|
|
65
|
+
>
|
|
66
|
+
<div className="ms-2 me-auto">
|
|
67
|
+
{renderResultItem(item, index)}
|
|
68
|
+
{/* {Object.keys(item.highlight).map((highlightKey, j) => (
|
|
69
|
+
<div
|
|
70
|
+
key={`highlight-${j}`}
|
|
71
|
+
className="item-highlight"
|
|
72
|
+
dangerouslySetInnerHTML={{__html: item.highlight[highlightKey][0]}}
|
|
73
|
+
/>
|
|
74
|
+
))} */}
|
|
75
|
+
<small className="fst-italic">{"displaySub"}</small>
|
|
76
|
+
</div>
|
|
77
|
+
{/* <span className="badge text-bg-light rounded-pill">
|
|
78
|
+
{item.fuzzy_score?.toFixed(2)}
|
|
79
|
+
</span> */}
|
|
80
|
+
{/* <span className="badge bg-primary rounded-pill">{item._score.toFixed(2)}</span> */}
|
|
81
|
+
</a>
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
})}
|
|
85
|
+
|
|
86
|
+
{/* children can be components like search history, etc */}
|
|
87
|
+
{children}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
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>
|
package/ui/SelectPills/index.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "./select-pills.scss"
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
type Props = {
|
|
4
5
|
direction?: "row" | "col";
|
|
5
6
|
size?: "md" | "sm";
|
|
@@ -16,11 +17,11 @@ export const SelectPills= ({
|
|
|
16
17
|
activeKey,
|
|
17
18
|
}: Props) => {
|
|
18
19
|
const handleSelectType = (type: string) => () => {
|
|
19
|
-
onChange(type)
|
|
20
|
-
}
|
|
20
|
+
onChange(type)
|
|
21
|
+
}
|
|
21
22
|
|
|
22
23
|
const renderRow = () => {
|
|
23
|
-
const iconSize = size === "sm" ? "24px" : "32px"
|
|
24
|
+
const iconSize = size === "sm" ? "24px" : "32px"
|
|
24
25
|
|
|
25
26
|
return (
|
|
26
27
|
<div className={`select-pills mb-3 card-group direction-row`}>
|
|
@@ -48,11 +49,11 @@ export const SelectPills= ({
|
|
|
48
49
|
</div>
|
|
49
50
|
))}
|
|
50
51
|
</div>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
52
|
+
)
|
|
53
|
+
}
|
|
53
54
|
|
|
54
55
|
const renderCol = () => {
|
|
55
|
-
const iconSize = size === "sm" ? "24px" : "32px"
|
|
56
|
+
const iconSize = size === "sm" ? "24px" : "32px"
|
|
56
57
|
|
|
57
58
|
return (
|
|
58
59
|
<div className="select-pills">
|
|
@@ -64,11 +65,14 @@ export const SelectPills= ({
|
|
|
64
65
|
onClick={handleSelectType(item.key)}
|
|
65
66
|
>
|
|
66
67
|
<div className="card-body d-flex flex-row px-2 py-3">
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
{item.icon && (
|
|
69
|
+
<img
|
|
70
|
+
className="d-flex align-self-center me-2"
|
|
71
|
+
style={{width: iconSize, height: iconSize}}
|
|
72
|
+
src={item.icon}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
|
|
72
76
|
{size === "md" && (
|
|
73
77
|
<div className="flex-column">
|
|
74
78
|
<h6 className="card-title mb-1 fw-bold">{item.name}</h6>
|
|
@@ -77,7 +81,7 @@ export const SelectPills= ({
|
|
|
77
81
|
)}
|
|
78
82
|
{size === "sm" && (
|
|
79
83
|
<div className="">
|
|
80
|
-
<h6 className="card-title my-0 d-inline-block me-
|
|
84
|
+
<h6 className="card-title my-0 d-inline-block me-1 fw-bold">{item.name}</h6>
|
|
81
85
|
<span className="card-text">{item.description}</span>
|
|
82
86
|
</div>
|
|
83
87
|
)}
|
|
@@ -85,8 +89,8 @@ export const SelectPills= ({
|
|
|
85
89
|
</div>
|
|
86
90
|
))}
|
|
87
91
|
</div>
|
|
88
|
-
)
|
|
89
|
-
}
|
|
92
|
+
)
|
|
93
|
+
}
|
|
90
94
|
|
|
91
|
-
return direction === "row" ? renderRow() : renderCol()
|
|
92
|
-
}
|
|
95
|
+
return direction === "row" ? renderRow() : renderCol()
|
|
96
|
+
}
|
|
@@ -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
|
|
|
@@ -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
|