diff --git a/src/core/components/sidepanels/panels/add-blocks/AddBlocks.tsx b/src/core/components/sidepanels/panels/add-blocks/AddBlocks.tsx index 349ca7b2..27b10af8 100644 --- a/src/core/components/sidepanels/panels/add-blocks/AddBlocks.tsx +++ b/src/core/components/sidepanels/panels/add-blocks/AddBlocks.tsx @@ -95,9 +95,9 @@ const AddBlocksPanel = ({ value={tab} className={mergeClasses("h-max")}> - {t("library")} - {t("blocks")} - {importHTMLSupport ? {t("import")} : null} + {t("Library")} + {t("Blocks")} + {importHTMLSupport ? {t("Import")} : null} {tab === "core" && ( diff --git a/src/core/locales/en.json b/src/core/locales/en.json index 43c0f575..565cacd3 100644 --- a/src/core/locales/en.json +++ b/src/core/locales/en.json @@ -469,5 +469,8 @@ "Fetching...": "Fetching...", "No images found": "No images found", "It looks like you haven't uploaded any images yet. Start by clicking the upload button above.": "It looks like you haven't uploaded any images yet. Start by clicking the upload button above.", - "Open Code Editor": "Open Code Editor" + "Open Code Editor": "Open Code Editor", + "Clear search": "Clear search", + "No results found for": "No results found for", + "Search {collectionName}": "Search {collectionName}" } \ No newline at end of file diff --git a/src/core/rjsf-widgets/link.tsx b/src/core/rjsf-widgets/link.tsx index 980dbf46..b879041b 100644 --- a/src/core/rjsf-widgets/link.tsx +++ b/src/core/rjsf-widgets/link.tsx @@ -1,8 +1,8 @@ import { FieldProps } from "@rjsf/utils"; import { map, split, get, isEmpty, debounce } from "lodash-es"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { useBuilderProp, useTranslation } from "../hooks"; -import { SearchIcon } from "lucide-react"; +import { SearchIcon, X } from "lucide-react"; import { CollectionItem } from "../types/chaiBuilderEditorProps"; const CollectionField = ({ @@ -18,9 +18,12 @@ const CollectionField = ({ const searchCollectionItems = useBuilderProp("searchCollectionItems", (_: string, __: any) => []); const [loading, setLoading] = useState(""); + const [isSearching, setIsSearching] = useState(false); const [collection, setCollection] = useState("pages"); const [searchQuery, setSearchQuery] = useState(""); const [collectionItems, setCollectionsItems] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(-1); + const listRef = useRef(null); const currentCollectionName = collections?.find((_collection) => _collection.key === collection)?.name; @@ -30,6 +33,7 @@ const CollectionField = ({ setCollection(_collection); setSearchQuery(""); setCollectionsItems([]); + setSelectedIndex(-1); (async () => { setLoading("FETCHING_INIT_VALUE"); @@ -50,6 +54,7 @@ const CollectionField = ({ setCollectionsItems(collectionItemResponse); } setLoading(""); + setSelectedIndex(-1); }, 300), [collection], ); @@ -58,6 +63,53 @@ const CollectionField = ({ const href = ["collection", collection, collectionItem.id]; if (!href[1]) return; onChange({ ...formData, href: href.join(":") }); + setIsSearching(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + setSelectedIndex((prev) => (prev < collectionItems.length - 1 ? prev + 1 : prev)); + break; + case "ArrowUp": + e.preventDefault(); + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev)); + break; + case "Enter": + e.preventDefault(); + if (collectionItems.length === 0) return; + + if (selectedIndex >= 0) { + handleSelect(collectionItems[selectedIndex]); + } + break; + case "Escape": + e.preventDefault(); + clearSearch(); + break; + } + }; + + useEffect(() => { + if (selectedIndex >= 0 && listRef.current) { + const selectedElement = listRef.current.children[selectedIndex] as HTMLElement; + selectedElement?.scrollIntoView({ block: "nearest" }); + } + }, [selectedIndex]); + + const clearSearch = () => { + setSearchQuery(""); + setCollectionsItems([]); + setSelectedIndex(-1); + setIsSearching(false); + }; + + const handleSearch = (query: string) => { + setSearchQuery(query); + setIsSearching(!isEmpty(query)); + setLoading("FETCHING_COLLECTION_ITEMS"); + getCollectionItems(query); }; return ( @@ -74,36 +126,54 @@ const CollectionField = ({ { - setSearchQuery(e.target.value); - setLoading("FETCHING_COLLECTION_ITEMS"); - getCollectionItems(e.target.value); - }} + onChange={(e) => handleSearch(e.target.value)} + onKeyDown={handleKeyDown} placeholder={t(`Search ${currentCollectionName}`)} disabled={loading === "FETCHING_INIT_VALUE"} - className="w-full rounded-md border border-gray-300 p-2" + className="w-full rounded-md border border-gray-300 p-2 pr-16" /> - +
+ {searchQuery && ( + + )} +
)} - {loading === "FETCHING_COLLECTION_ITEMS" ? ( -
-
-
+ + {(loading === "FETCHING_COLLECTION_ITEMS" || + !isEmpty(collectionItems) || + (isSearching && isEmpty(collectionItems))) && ( +
+ {loading === "FETCHING_COLLECTION_ITEMS" ? ( +
+
+
+
+ ) : isSearching && isEmpty(collectionItems) ? ( +
+ {t("No results found for")} "{searchQuery}" +
+ ) : ( +
    + {map(collectionItems?.slice(0, 20), (item, index) => ( +
  • handleSelect(item)} + className={`cursor-pointer p-2 text-xs ${ + formData?.href?.includes(item.id) + ? "bg-blue-200" + : index === selectedIndex + ? "bg-gray-100" + : "hover:bg-gray-100" + }`}> + {item.name} {item.slug && ( {item.slug} )} +
  • + ))} +
+ )}
- ) : ( - !isEmpty(collectionItems) && ( -
    - {map(collectionItems?.slice(0, 20), (item) => ( -
  • handleSelect(item)} - className={`cursor-pointer p-2 text-xs ${formData?.href?.includes(item.id) ? "bg-blue-200" : "hover:bg-gray-100"}`}> - {item.name} {item.slug && ( {item.slug} )} -
  • - ))} -
- ) )}
);