Skip to content

Commit 0d803f6

Browse files
committed
special nodata/noresults handlers
1 parent 4f41250 commit 0d803f6

File tree

2 files changed

+111
-31
lines changed

2 files changed

+111
-31
lines changed

frontend/app/suggestion/suggestion.tsx

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface SuggestionControlProps {
1717
fetchSuggestions: SuggestionsFnType;
1818
className?: string;
1919
placeholderText?: string;
20+
children?: React.ReactNode;
2021
}
2122

2223
type BlockHeaderSuggestionControlProps = Omit<SuggestionControlProps, "anchorRef" | "isOpen"> & {
@@ -31,10 +32,11 @@ const SuggestionControl: React.FC<SuggestionControlProps> = ({
3132
onSelect,
3233
fetchSuggestions,
3334
className,
35+
children,
3436
}) => {
3537
if (!isOpen || !anchorRef.current || !fetchSuggestions) return null;
3638

37-
return <SuggestionControlInner {...{ anchorRef, onClose, onSelect, fetchSuggestions, className }} />;
39+
return <SuggestionControlInner {...{ anchorRef, onClose, onSelect, fetchSuggestions, className, children }} />;
3840
};
3941

4042
function highlightPositions(target: string, positions: number[]): ReactNode[] {
@@ -50,7 +52,11 @@ function highlightPositions(target: string, positions: number[]): ReactNode[] {
5052

5153
while (targetIndex < target.length) {
5254
if (posIndex < positions.length && targetIndex === positions[posIndex]) {
53-
result.push(<span className="text-blue-500 font-bold">{target[targetIndex]}</span>);
55+
result.push(
56+
<span key={`h-${targetIndex}`} className="text-blue-500 font-bold">
57+
{target[targetIndex]}
58+
</span>
59+
);
5460
posIndex++;
5561
} else {
5662
result.push(target[targetIndex]);
@@ -138,19 +144,42 @@ const BlockHeaderSuggestionControl: React.FC<BlockHeaderSuggestionControlProps>
138144
return <SuggestionControl {...props} anchorRef={{ current: headerElem }} isOpen={isOpen} className={newClass} />;
139145
};
140146

141-
const SuggestionControlInner: React.FC<Omit<SuggestionControlProps, "isOpen">> = ({
147+
/**
148+
* The empty state component that can be used as a child of SuggestionControl.
149+
* If no children are provided to SuggestionControl, this default empty state will be used.
150+
*/
151+
const SuggestionControlNoResults: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
152+
return (
153+
<div className="flex items-center justify-center min-h-[120px] p-4">
154+
{children ?? <span className="text-gray-500">No Suggestions</span>}
155+
</div>
156+
);
157+
};
158+
159+
const SuggestionControlNoData: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
160+
return (
161+
<div className="flex items-center justify-center min-h-[120px] p-4">
162+
{children ?? <span className="text-gray-500">No Suggestions</span>}
163+
</div>
164+
);
165+
};
166+
167+
interface SuggestionControlInnerProps extends Omit<SuggestionControlProps, "isOpen"> {}
168+
169+
const SuggestionControlInner: React.FC<SuggestionControlInnerProps> = ({
142170
anchorRef,
143171
onClose,
144172
onSelect,
145173
onTab,
146174
fetchSuggestions,
147175
className,
148176
placeholderText,
177+
children,
149178
}) => {
150179
const widgetId = useId();
151180
const [query, setQuery] = useState("");
152181
const reqNumRef = useRef(0);
153-
const [suggestions, setSuggestions] = useState<SuggestionType[]>([]);
182+
let [suggestions, setSuggestions] = useState<SuggestionType[]>([]);
154183
const [selectedIndex, setSelectedIndex] = useState(0);
155184
const [fetched, setFetched] = useState(false);
156185
const inputRef = useRef<HTMLInputElement>(null);
@@ -160,6 +189,12 @@ const SuggestionControlInner: React.FC<Omit<SuggestionControlProps, "isOpen">> =
160189
strategy: "absolute",
161190
middleware: [offset(-1)],
162191
});
192+
const emptyStateChild = React.Children.toArray(children).find(
193+
(child) => React.isValidElement(child) && child.type === SuggestionControlNoResults
194+
);
195+
const noDataChild = React.Children.toArray(children).find(
196+
(child) => React.isValidElement(child) && child.type === SuggestionControlNoData
197+
);
163198

164199
useEffect(() => {
165200
refs.setReference(anchorRef.current);
@@ -168,7 +203,7 @@ const SuggestionControlInner: React.FC<Omit<SuggestionControlProps, "isOpen">> =
168203
useEffect(() => {
169204
reqNumRef.current++;
170205
fetchSuggestions(query, { widgetid: widgetId, reqnum: reqNumRef.current }).then((results) => {
171-
if (results.reqnum != reqNumRef.current) {
206+
if (results.reqnum !== reqNumRef.current) {
172207
return;
173208
}
174209
setSuggestions(results.suggestions ?? []);
@@ -222,7 +257,6 @@ const SuggestionControlInner: React.FC<Omit<SuggestionControlProps, "isOpen">> =
222257
}
223258
}
224259
};
225-
226260
return (
227261
<div
228262
className={clsx(
@@ -247,29 +281,37 @@ const SuggestionControlInner: React.FC<Omit<SuggestionControlProps, "isOpen">> =
247281
placeholder={placeholderText}
248282
/>
249283
</div>
250-
{fetched && suggestions.length > 0 && (
251-
<div ref={dropdownRef} className="max-h-96 overflow-y-auto divide-y divide-gray-700">
252-
{suggestions.map((suggestion, index) => (
253-
<div
254-
key={suggestion.suggestionid}
255-
className={clsx(
256-
"flex items-center gap-3 px-4 py-2 cursor-pointer",
257-
index === selectedIndex ? "bg-accentbg" : "hover:bg-hoverbg",
258-
"text-gray-100"
259-
)}
260-
onClick={() => {
261-
onSelect(suggestion, query);
262-
onClose();
263-
}}
264-
>
265-
<SuggestionIcon suggestion={suggestion} />
266-
<SuggestionContent suggestion={suggestion} />
267-
</div>
268-
))}
269-
</div>
270-
)}
284+
{fetched &&
285+
(suggestions.length > 0 ? (
286+
<div ref={dropdownRef} className="max-h-96 overflow-y-auto divide-y divide-gray-700">
287+
{suggestions.map((suggestion, index) => (
288+
<div
289+
key={suggestion.suggestionid}
290+
className={clsx(
291+
"flex items-center gap-3 px-4 py-2 cursor-pointer",
292+
index === selectedIndex ? "bg-accentbg" : "hover:bg-hoverbg",
293+
"text-gray-100"
294+
)}
295+
onClick={() => {
296+
onSelect(suggestion, query);
297+
onClose();
298+
}}
299+
>
300+
<SuggestionIcon suggestion={suggestion} />
301+
<SuggestionContent suggestion={suggestion} />
302+
</div>
303+
))}
304+
</div>
305+
) : (
306+
// Render the empty state (either a provided child or the default)
307+
<div key="empty" className="flex items-center justify-center min-h-[120px] p-4">
308+
{query === ""
309+
? (noDataChild ?? <SuggestionControlNoData />)
310+
: (emptyStateChild ?? <SuggestionControlNoResults />)}
311+
</div>
312+
))}
271313
</div>
272314
);
273315
};
274316

275-
export { BlockHeaderSuggestionControl, SuggestionControl };
317+
export { BlockHeaderSuggestionControl, SuggestionControl, SuggestionControlNoData, SuggestionControlNoResults };

frontend/app/view/webview/webview.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33

44
import { BlockNodeModel } from "@/app/block/blocktypes";
55
import { Search, useSearch } from "@/app/element/search";
6-
import { getApi, getBlockMetaKeyAtom, getSettingsKeyAtom, openLink } from "@/app/store/global";
6+
import { createBlock, getApi, getBlockMetaKeyAtom, getSettingsKeyAtom, openLink } from "@/app/store/global";
77
import { getSimpleControlShiftAtom } from "@/app/store/keymodel";
88
import { ObjectService } from "@/app/store/services";
99
import { RpcApi } from "@/app/store/wshclientapi";
1010
import { TabRpcClient } from "@/app/store/wshrpcutil";
11-
import { BlockHeaderSuggestionControl } from "@/app/suggestion/suggestion";
11+
import {
12+
BlockHeaderSuggestionControl,
13+
SuggestionControlNoData,
14+
SuggestionControlNoResults,
15+
} from "@/app/suggestion/suggestion";
1216
import { WOS, globalStore } from "@/store/global";
1317
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
1418
import { fireAndForget } from "@/util/util";
@@ -601,6 +605,19 @@ interface WebViewProps {
601605

602606
const BookmarkTypeahead = memo(
603607
({ model, blockRef }: { model: WebViewModel; blockRef: React.RefObject<HTMLDivElement> }) => {
608+
const openBookmarksJson = () => {
609+
fireAndForget(async () => {
610+
const path = `${getApi().getConfigDir()}/presets/bookmarks.json`;
611+
const blockDef: BlockDef = {
612+
meta: {
613+
view: "preview",
614+
file: path,
615+
},
616+
};
617+
await createBlock(blockDef, false, true);
618+
model.setTypeaheadOpen(false);
619+
});
620+
};
604621
return (
605622
<BlockHeaderSuggestionControl
606623
blockRef={blockRef}
@@ -614,7 +631,28 @@ const BookmarkTypeahead = memo(
614631
}}
615632
fetchSuggestions={model.fetchBookmarkSuggestions}
616633
placeholderText="Open Bookmark..."
617-
/>
634+
>
635+
<SuggestionControlNoData>
636+
<div className="text-center">
637+
<p className="text-lg font-bold text-gray-100">No Bookmarks Configured</p>
638+
<p className="text-sm text-gray-400 mt-1">
639+
Edit your <code className="font-mono">bookmarks.json</code> file to configure bookmarks.
640+
</p>
641+
<button
642+
onClick={openBookmarksJson}
643+
className="mt-3 px-4 py-2 text-sm font-medium text-white bg-gray-700 rounded-lg hover:bg-gray-600 cursor-pointer"
644+
>
645+
Open bookmarks.json
646+
</button>
647+
</div>
648+
</SuggestionControlNoData>
649+
650+
<SuggestionControlNoResults>
651+
<div className="text-center">
652+
<p className="text-sm text-gray-400">No matching bookmarks</p>
653+
</div>
654+
</SuggestionControlNoResults>
655+
</BlockHeaderSuggestionControl>
618656
);
619657
}
620658
);

0 commit comments

Comments
 (0)