@rpcbase/client 0.194.0 → 0.196.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/auth/getUid.js CHANGED
@@ -3,7 +3,7 @@ import {Platform} from "react-native"
3
3
 
4
4
  import {privateGetUid} from "./index"
5
5
 
6
- const getUid = Platform.OS === "web" ? privateGetUid : () => {
6
+ export const getUid = Platform.OS === "web" ? privateGetUid : () => {
7
7
  console.warn("native should not call getUid, use userId from AuthContext")
8
8
  return null
9
9
  }
package/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  /* @flow */
2
2
  import useRPC from "./helpers/useRPC"
3
3
  import useStoredValue from "./helpers/useStoredValue"
4
- import getUid from "./auth/getUid"
5
4
 
6
- export {getUid, useRPC, useStoredValue}
5
+ export {useRPC, useStoredValue}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/client",
3
- "version": "0.194.0",
3
+ "version": "0.196.0",
4
4
  "scripts": {
5
5
  "build": "../../node_modules/.bin/wireit",
6
6
  "test": "../../node_modules/.bin/wireit"
@@ -91,15 +91,17 @@
91
91
  "figma-squircle": "0.3.1",
92
92
  "firebase": "10.12.3",
93
93
  "framer-motion": "11.3.22",
94
+ "fuzzysort": "3.0.2",
94
95
  "i18next": "23.12.2",
95
96
  "i18next-chained-backend": "4.6.2",
96
97
  "i18next-resources-to-backend": "1.2.1",
97
98
  "js-tree": "2.0.2",
98
99
  "lz-string": "1.5.0",
99
100
  "posthog-js": "1.147.0",
100
- "pouchdb-adapter-indexeddb": "8.0.1",
101
- "pouchdb-core": "8.0.1",
102
- "pouchdb-find": "8.0.1",
101
+ "pouchdb-adapter-indexeddb": "9.0.0",
102
+ "pouchdb-core": "9.0.0",
103
+ "pouchdb-find": "9.0.0",
104
+ "react-bootstrap-typeahead": "6.3.2",
103
105
  "react-i18next": "15.0.1",
104
106
  "rrweb": "1.1.3",
105
107
  "socket.io-client": "4.7.5",
@@ -1,5 +1,4 @@
1
1
  /* @flow */
2
- import assert from "assert"
3
2
  import {Platform} from "react-native"
4
3
  import {useCallback, useEffect, useState, useMemo, useId, useRef} from "react"
5
4
  import debug from "debug"
@@ -25,9 +24,10 @@ const getUseQuery = (register_query) => (
25
24
  ) => {
26
25
  const id = useId()
27
26
 
27
+ // TODO: should the uid be gone here ? it's part of the auth layer, not this here
28
28
  // TODO: retrieve this from future AuthContext in client
29
29
  const uid = useMemo(() => {
30
- // TODO: why is there a options.userId here ??
30
+ // TODO: why is there a options.userId here?? (it was for mobile we need to unify this)
31
31
  const _uid = Platform.OS === "web" ? getUid() : options.userId
32
32
  return _uid
33
33
  }, [])
@@ -75,12 +75,16 @@ const getUseQuery = (register_query) => (
75
75
  // set data in a ref so that it doesn't force re-rendering ie: unsubscribe / resubscribe
76
76
  dataRef.current = newData
77
77
 
78
+ // useStorage currently used as a fast local cache, indexedDB
78
79
  // we only save network queries
80
+ // TODO: use localstorage in react native and pouchdb everywhere
79
81
  if (useStorage && context.source === "network") {
80
82
  if (Platform.OS === "web") {
81
83
  localStorage.setItem(storageKey, LZString.compressToUTF16(JSON.stringify(newData)))
82
84
  } else {
85
+ // TODO: this is done in pouchDB nOW ?????
83
86
  // TODO: RN MMKV
87
+ console.log("mmkv NYI")
84
88
  }
85
89
  }
86
90
 
@@ -147,7 +151,7 @@ const getUseQuery = (register_query) => (
147
151
  newData = _omit(queryResult, "__txn_id")
148
152
  }
149
153
 
150
- // We return once in any case!
154
+ // we return once in any case
151
155
  if (!hasFirstReply.current) {
152
156
  hasFirstReply.current = true
153
157
 
@@ -168,7 +172,7 @@ const getUseQuery = (register_query) => (
168
172
  return
169
173
  }
170
174
 
171
- if (isEqual(data, newData) && !isEqualValues(data, newData) && __DEV__) {
175
+ if (__DEV__ && isEqual(data, newData) && !isEqualValues(data, newData)) {
172
176
  alert("EQUALITY MISMATCH THIS SHOULD NOT HAPPEN!", data, newData)
173
177
  }
174
178
 
@@ -191,11 +195,11 @@ const getUseQuery = (register_query) => (
191
195
 
192
196
 
193
197
  const loadNextPage = useCallback(() => {
194
- console.log("NYI: will load next page after DOC")
198
+ console.log("loadNextPage NYI")
195
199
  }, [])
196
200
 
197
201
 
198
- const result = useMemo(() => ({data, source, error, loading}), [data, source, error, loading])
202
+ const result = useMemo(() => ({data, source, error, loading, loadNextPage}), [data, source, error, loading, loadNextPage])
199
203
 
200
204
  // TODO:
201
205
  // if (Array.isArray(result.data) && !result.source) {
package/rts/rts.js CHANGED
@@ -10,6 +10,7 @@ import getBaseUrl from "../getBaseUrl"
10
10
 
11
11
  import store from "./store"
12
12
 
13
+
13
14
  const log = debug("rb:socket")
14
15
 
15
16
  const TENANT_ID_HEADER = "rb-tenant-id"
@@ -197,8 +198,6 @@ export const register_query = (model_name, query, _options, _callback) => {
197
198
  }
198
199
  _queries_store[model_name][query]
199
200
 
200
- console.log("QUERY OPTIONSSS", options)
201
-
202
201
  // TODO: why both run and register query here ? the run_query should come straight from register ?
203
202
  _socket.emit("run_query", {model_name, query, query_key, options})
204
203
  _socket.emit("register_query", {model_name, query, query_key, options})
@@ -6,22 +6,26 @@ import FindPlugin from "pouchdb-find"
6
6
 
7
7
  import {RB_TENANT_ID, RB_APP_NAME} from "env"
8
8
 
9
+
9
10
  const log = debug("rb:rts:store")
10
11
 
11
- let prefix = `rb/${RB_TENANT_ID}/`
12
+ let prefix = `rb/`
12
13
 
13
14
  if (RB_APP_NAME) prefix += `${RB_APP_NAME}/`
14
15
 
16
+ prefix += `${RB_TENANT_ID}/`
17
+
18
+ log("prefix:", prefix)
19
+
15
20
  PouchDB.prefix = prefix
16
21
 
17
22
  PouchDB.plugin(IndexedDBAdapter)
18
23
  PouchDB.plugin(FindPlugin)
19
24
 
20
-
21
25
  const _cols_store = Object.create(null)
22
26
 
23
-
24
27
  const get_collection = (col_name) => {
28
+
25
29
  if (!col_name) {
26
30
  console.warn("supplied invalid / empty collection name to rts")
27
31
  }
@@ -0,0 +1,41 @@
1
+ import {formatDistance} from "date-fns/formatDistance"
2
+
3
+ import {useSearchHistory} from "./useSearchHistory"
4
+
5
+ import "./search-history.scss"
6
+
7
+ // TODO: refactor out of rbt to be able to control the input
8
+ // TODO: search history must support keyboard navigation
9
+
10
+ // we pass the searchHistory prop from the top component
11
+ // because we are unmounted when results aren't shown,
12
+ // but we want history to be loaded anyway
13
+ export const SearchHistory = ({setSearchInput}) => {
14
+ const searchHistory = useSearchHistory()
15
+
16
+ const getClickHandler = (item) => (e) => {
17
+ e.preventDefault()
18
+ setSearchInput(item.query)
19
+ }
20
+
21
+ return (
22
+ <>
23
+ <div className="search-history-header ps-3 pb-2 mt-2 fw-bold border-bottom">
24
+ Previous Queries
25
+ </div>
26
+ {searchHistory.map((item, i) => (
27
+ <div
28
+ role="button"
29
+ key={`search-history-${i}`}
30
+ className="list-group-item list-group-item-action"
31
+ onClick={getClickHandler(item)}
32
+ >
33
+ <span>{item.query}</span>
34
+ <small className="text-secondary ms-2">
35
+ {formatDistance(item.timestamp, new Date(), {addSuffix: true})}
36
+ </small>
37
+ </div>
38
+ ))}
39
+ </>
40
+ )
41
+ }
@@ -0,0 +1,9 @@
1
+ @import "helpers";
2
+
3
+ .search-history-header {
4
+ // background: red;
5
+ color: $gray-500;
6
+ font-size: 0.875rem;
7
+
8
+ // text-transform: uppercase;
9
+ }
@@ -0,0 +1,56 @@
1
+ /* @flow */
2
+ import {useEffect, useState} from "react"
3
+
4
+ import {useQuery} from "@rpcbase/client/rts"
5
+ import {getUid} from "@rpcbase/client/auth/getUid"
6
+
7
+ import stripDiacritics from "react-bootstrap-typeahead/cjs/utils/stripDiacritics"
8
+
9
+ const HISTORY_MAX_RESULTS = 10
10
+
11
+ export const useSearchHistory = () => {
12
+ const user_id = getUid()
13
+
14
+ const [history, setHistory] = useState([])
15
+ const historyQuery = useQuery("PersistedEvent", {_owners: {$in: [user_id]}, type: "search_query"})
16
+
17
+ // Initial load history
18
+ useEffect(() => {
19
+ if (!historyQuery.data) return
20
+
21
+ // map sorted items
22
+ const sorted_items = historyQuery.data
23
+ .filter((item) => !!item.data)
24
+ .sort((a, b) => new Date(b._created_at) - new Date(a._created_at))
25
+ .map((item) => {
26
+ const query = item.data.query
27
+ return {
28
+ query,
29
+ query_key: stripDiacritics(query.toLowerCase()),
30
+ timestamp: new Date(item._created_at),
31
+ }
32
+ })
33
+
34
+ // add values and check for unicity
35
+ const results = []
36
+ for (let i = 0; i < sorted_items.length; i++) {
37
+ // stop if we have reached results limit
38
+ if (results.length > HISTORY_MAX_RESULTS) {
39
+ break
40
+ }
41
+
42
+ const item = sorted_items[i]
43
+
44
+ // before inserting into results, check if there is another occurrence in results
45
+ if (results.find((o) => o.query_key === item.query_key)) {
46
+ continue
47
+ }
48
+
49
+ results.push(item)
50
+ }
51
+
52
+ setHistory(results)
53
+ }, [historyQuery.data, setHistory])
54
+
55
+ return history
56
+ }
@@ -0,0 +1,6 @@
1
+
2
+ const getActions = (pathname) => {
3
+ return []
4
+ }
5
+
6
+ export default getActions
@@ -0,0 +1,188 @@
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
+ }
@@ -0,0 +1,59 @@
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
@@ -0,0 +1,301 @@
1
+ import assert from "assert"
2
+ import {useEffect, useState, useContext, useRef, useCallback} from "react"
3
+ import {Typeahead} from "react-bootstrap-typeahead"
4
+ import Overlay from "react-bootstrap/Overlay"
5
+ import _throttle from "lodash/throttle"
6
+
7
+ import ActivityIndicator from "../ActivityIndicator"
8
+
9
+ // import ItemContext from "helpers/ItemContext"
10
+ // import {useEnvContext} from "helpers/EnvContext"
11
+
12
+ import {SearchResults} from "./SearchResults"
13
+ import {SearchHistory} from "./SearchHistory"
14
+
15
+ // import search_anything from "rpc!server/search-indexer/search_anything"
16
+ // import search_advanced from "rpc!server/search-indexer/advanced/search_advanced"
17
+ // import get_item_info from "rpc!server/items/get_item_info"
18
+
19
+ import "./search.scss"
20
+
21
+ // import stripDiacritics from "react-bootstrap-typeahead/cjs/utils/stripDiacritics"
22
+ // TODO: at this point we don't use any rbt features, it's best to remove it from here
23
+ // and reimplement the token + input rendering ourselves
24
+
25
+ const MIN_SEARCH_LENGTH = 2
26
+ // TODO: i18n placeholder
27
+ const PLACEHOLDER = "Search..."
28
+
29
+ export const Search = ({
30
+ minLength = MIN_SEARCH_LENGTH,
31
+ onSearch,
32
+ }: {
33
+ minLength?: number
34
+ onSearch: (queryStr: string) => Array<any>
35
+ }) => {
36
+ // const envContext = useEnvContext()
37
+ // const itemContext = useContext(ItemContext)
38
+
39
+ const typeaheadRef = useRef()
40
+ const wrapperRef = useRef()
41
+
42
+ const [results, setResults] = useState([])
43
+ const [isLoading, setIsLoading] = useState(false)
44
+ const [isFocused, setIsFocused] = useState(false)
45
+ const [showResults, setShowResults] = useState(false)
46
+ const [currentInputValue, setCurrentInputValue] = useState("")
47
+
48
+ const [selected, setSelected] = useState([{id: null, name: ""}])
49
+
50
+ const onShortcutToggle = () => {
51
+ if (!showResults) {
52
+ typeaheadRef.current?.focus()
53
+ }
54
+ if (showResults) {
55
+ setShowResults(false)
56
+ }
57
+ }
58
+
59
+ useEffect(() => {
60
+ // this event triggerred from hot keys in router as it must be a global listener
61
+ document.body.addEventListener("toggle-search-anything", onShortcutToggle)
62
+
63
+ return () => document.body.removeEventListener("toggle-search-anything", onShortcutToggle)
64
+ }, [])
65
+
66
+ useEffect(() => {
67
+ // TODO: do not load if name in cache?
68
+ // TODO: reapply selection when navigation changes
69
+ // if (itemContext.id && selected.length > 0 && itemContext.id !== selected[0]?.id) {
70
+ // const load = async() => {
71
+ // const res = await get_item_info({id: itemContext.id, col: itemContext.col})
72
+ // assert(res.status === "ok")
73
+ // setSelected([{id: itemContext.id, col: itemContext.col, name: res.name}])
74
+ // }
75
+ // load()
76
+ // }
77
+ // }, [itemContext, selected, setSelected])
78
+ }, [])
79
+
80
+ const onFocus = (e: SyntheticFocusEvent<HTMLElement>) => {
81
+ setShowResults(true)
82
+ setIsFocused(true)
83
+
84
+ // TODO: we should only select it when we are sure the user didn't lose focus
85
+ // by focusing out of the tab for instance
86
+ // Select the input text when focusing
87
+ typeaheadRef.current.getInput().select()
88
+ // if (e.type === "click") {
89
+ // return
90
+ // }
91
+ }
92
+
93
+ const onBlur = () => {
94
+ console.log("onBlur: disabling menu")
95
+ // TODO: we should hide the menu in most blur cases
96
+ setIsFocused(false)
97
+ }
98
+
99
+ const onChange = (selection) => {
100
+ assert([0, 1].includes(selection.length), "unknown selection length")
101
+
102
+ console.log("selection change", selection)
103
+
104
+ if (selection.length === 0) {
105
+ console.log("apply selection change")
106
+ setSelected([])
107
+ return
108
+ }
109
+ }
110
+
111
+ const onSearchCallback = useRef(
112
+ _throttle(
113
+ async(queryStr: string) => {
114
+ // used to not set loader before 100ms delay (if search is too fast)
115
+ const tm = setTimeout(() => {
116
+ setIsLoading(true)
117
+ }, 100)
118
+
119
+ // const res = await search_anything({
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)
128
+
129
+ setResults(results)
130
+ clearTimeout(tm)
131
+ setIsLoading(false)
132
+ },
133
+ 200,
134
+ {leading: true, trailing: true},
135
+ ),
136
+ ).current
137
+
138
+ // const onSearchAdvanced = useRef(
139
+ // _throttle(
140
+ // async(str: string) => {
141
+ // const search_context = selected[0]
142
+ // let res
143
+ // // TMP
144
+ // try {
145
+ // // res = await search_advanced({
146
+ // // group_id: envContext.groupId,
147
+ // // query: str,
148
+ // // search_context,
149
+ // // })
150
+ // console.log("TMP: search advanced is disabled", search_context, search_advanced)
151
+ // } catch (err) {
152
+ // console.log("advanced search got error", err)
153
+ // return
154
+ // }
155
+ // console.log("onSearchAdvanced res", res)
156
+ // },
157
+ // 1000,
158
+ // {leading: false},
159
+ // ),
160
+ // ).current
161
+
162
+ // perform search when input changes
163
+ useEffect(() => {
164
+ const val = currentInputValue
165
+ if (val.length >= minLength) {
166
+ onSearchCallback(val)
167
+ } else {
168
+ console.log("TODO: search input < minLength, clear results")
169
+ }
170
+ }, [currentInputValue])
171
+
172
+ const onInputChange = (v) => {
173
+ setCurrentInputValue(v)
174
+ }
175
+
176
+ const onHideResults = (e) => {
177
+ // console.log("hide results wow", e)
178
+ // when clicking on anything inside the input, we do not close menu
179
+ if (e instanceof MouseEvent) {
180
+ const {target} = e
181
+ if (wrapperRef.current?.contains(target)) {
182
+ return
183
+ }
184
+ }
185
+ // also blur the input when closing the menu
186
+ // typeaheadRef.current?.clear()
187
+ typeaheadRef.current?.blur()
188
+ setShowResults(false)
189
+ }
190
+
191
+ // clears the input (but preserves the token "scope" if there is one)
192
+ const getClearHandler = (onClear) => () => {
193
+ typeaheadRef.current?.clear()
194
+ typeaheadRef.current?.focus()
195
+
196
+ // requestAnimationFrame(() => {
197
+ // // this.typeaheadRef.current?.focus()
198
+ // })
199
+ }
200
+
201
+ const setSearchInput = (v) => {
202
+ setCurrentInputValue(v)
203
+ }
204
+
205
+ const onCompleteSearch = useCallback(() => {
206
+ typeaheadRef.current?.blur()
207
+ typeaheadRef.current?.clear()
208
+ setCurrentInputValue("")
209
+ setShowResults(false)
210
+ }, [])
211
+
212
+ const showClearButton = () => !isLoading && currentInputValue !== ""
213
+
214
+ const hasAdvancedResults = false
215
+
216
+ return (
217
+ <>
218
+ <div
219
+ id="search-anything-wrapper"
220
+ ref={wrapperRef}
221
+ className="d-flex mx-auto w-100"
222
+ // style={{height: 32, position: "relative", maxWidth: 804}}
223
+ style={{height: 32, position: "relative"}}
224
+ >
225
+ <Typeahead
226
+ id="search-anything"
227
+ ref={typeaheadRef}
228
+ className={cx("search-anything-typeahead w-100", {"is-focused": isFocused})}
229
+ placeholder={PLACEHOLDER}
230
+ inputProps={{
231
+ placeholder: PLACEHOLDER,
232
+ }}
233
+ labelKey={"name"}
234
+ renderMenu={() => null}
235
+ onInputChange={onInputChange}
236
+ multiple
237
+ allowNew={true} // is this prop doing anything in our case?
238
+ onFocus={onFocus}
239
+ onBlur={onBlur}
240
+ onChange={onChange}
241
+ selected={selected}
242
+ options={selected}
243
+ >
244
+ {({onClear}) => (
245
+ <div className="rbt-aux">
246
+ {showClearButton() && (
247
+ <button
248
+ className="btn btn-link link-secondary me-2"
249
+ onClick={getClearHandler(onClear)}
250
+ style={{display: "block", zIndex: 1, pointerEvents: "auto"}}
251
+ >
252
+ clear
253
+ </button>
254
+ )}
255
+ {isLoading && <ActivityIndicator />}
256
+ {hasAdvancedResults && (
257
+ <div>
258
+ <kbd>⌘+↵</kbd>
259
+ </div>
260
+ )}
261
+ </div>
262
+ )}
263
+ </Typeahead>
264
+ </div>
265
+
266
+ <Overlay
267
+ target={wrapperRef.current}
268
+ container={wrapperRef.current}
269
+ placement="bottom"
270
+ show={showResults}
271
+ transition={false}
272
+ onHide={onHideResults}
273
+ rootClose={true}
274
+ rootCloseEvent={"mousedown"}
275
+ >
276
+ {({style, ...overlayProps}) => (
277
+ <div
278
+ id="search-anything-overlay"
279
+ {...overlayProps}
280
+ className="shadow-lg"
281
+ style={{
282
+ ...style,
283
+ top: 7,
284
+ left: -1,
285
+ right: -3,
286
+ }}
287
+ >
288
+ <SearchResults
289
+ query={currentInputValue.trim()}
290
+ results={results}
291
+ onCompleteSearch={onCompleteSearch}
292
+ searchContext={selected[0]}
293
+ >
294
+ <SearchHistory setSearchInput={setSearchInput} />
295
+ </SearchResults>
296
+ </div>
297
+ )}
298
+ </Overlay>
299
+ </>
300
+ )
301
+ }
File without changes
@@ -1,7 +1,6 @@
1
1
  /* @flow */
2
2
  import {forwardRef} from "react"
3
- import Tooltip from "react-bootstrap/Tooltip"
4
- import OverlayTrigger from "react-bootstrap/OverlayTrigger"
3
+ import {OverlayTrigger, Tooltip} from "react-bootstrap"
5
4
 
6
5
 
7
6
  export const View = forwardRef(
@@ -0,0 +1,42 @@
1
+ import {useState, useEffect} from "react"
2
+
3
+ export const useActiveListItemIndex = ({items, activeItem, onSelectItem}) => {
4
+ const [activeItemIndex, setActiveItemIndex] = useState(() => {
5
+ let initialIndex = items.findIndex((item) => item.obj?._id === activeItem?._id)
6
+ if (!initialIndex) initialIndex = 0
7
+ return initialIndex
8
+ })
9
+
10
+ useEffect(() => {
11
+ const onKeyDown = (e) => {
12
+ if (e.key === "ArrowDown") {
13
+ // increment
14
+ if (activeItemIndex < items.length - 1) setActiveItemIndex(activeItemIndex + 1)
15
+ // cycle back to 0
16
+ else setActiveItemIndex(0)
17
+ } else if (e.key === "ArrowUp") {
18
+ // decrement
19
+ if (activeItemIndex > 0) setActiveItemIndex(activeItemIndex - 1)
20
+ // cycle back to almost last item
21
+ else setActiveItemIndex(items.length - 1)
22
+ } else if (e.key === "Enter") {
23
+ onSelectItem(activeItemIndex)
24
+ }
25
+ }
26
+
27
+ document.addEventListener("keydown", onKeyDown)
28
+
29
+ return () => {
30
+ document.removeEventListener("keydown", onKeyDown)
31
+ }
32
+ }, [activeItemIndex, items, onSelectItem])
33
+
34
+ // reset when out of bounds (or items changed)
35
+ useEffect(() => {
36
+ if (activeItemIndex > items.length) {
37
+ setActiveItemIndex(0)
38
+ }
39
+ }, [activeItemIndex, items])
40
+
41
+ return {activeItemIndex}
42
+ }
@@ -0,0 +1,14 @@
1
+ import {useEffect} from "react"
2
+
3
+
4
+ export const useScrollSelectorIntoView = (selector) => {
5
+ // scroll element into view if necessary
6
+ useEffect(() => {
7
+ const el = document.querySelector(selector)
8
+ if (!el) return
9
+ // TODO: check if element is visible and only scroll if it's not visible
10
+ // https://htmldom.dev/check-if-an-element-is-visible-in-a-scrollable-container/
11
+
12
+ el?.scrollIntoView({behavior: "smooth", block: "end", inline: "nearest"})
13
+ }, [selector])
14
+ }