diff --git a/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemParent.tsx b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemParent.tsx index 17bd68b1c..182a1030d 100644 --- a/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemParent.tsx +++ b/src/apps/content-editor/src/app/views/ItemEdit/Meta/settings/ItemParent.tsx @@ -1,3 +1,4 @@ +import { FocusEvent, useEffect, useState } from "react"; import { Box, Autocomplete, TextField, ListItem } from "@mui/material"; import { FieldShell } from "../../../../components/Editor/Field/FieldShell"; import { useDispatch, useSelector } from "react-redux"; @@ -7,11 +8,12 @@ import { ContentNavItem, } from "../../../../../../../../shell/services/types"; import { debounce, uniqBy } from "lodash"; -import { useEffect, useState } from "react"; import { useLocation, useParams } from "react-router"; import { notify } from "../../../../../../../../shell/store/notifications"; import { searchItems } from "../../../../../../../../shell/store/content"; import { useGetContentNavItemsQuery } from "../../../../../../../../shell/services/instance"; +import { sortMostRelevantSearch } from "../../../../../../../../shell/components/GlobalSearch/utils"; +import zuid from "zuid"; type ParentOption = { value: string; @@ -103,8 +105,11 @@ export const ItemParent = ({ onChange }: ItemParentProps) => { getParentOptions(item?.meta?.langID, item?.web?.path, items) ); const [isLoadingOptions, setIsLoadingOptions] = useState(false); + const [sortedOptions, setSortedOptions] = useState(options); + const [searchText, setSearchText] = useState(""); const handleSearchOptions = debounce((filterTerm) => { + setSearchText(filterTerm); if (filterTerm) { dispatch(searchItems(filterTerm)) // @ts-expect-error untyped @@ -213,6 +218,27 @@ export const ItemParent = ({ onChange }: ItemParentProps) => { } }, [rawNavData]); + useEffect(() => { + const normalizedSearch = searchText.toLowerCase(); + const isZuid = zuid.isValid(normalizedSearch); + const filteredOptions = isZuid + ? options.filter( + (option) => option?.value.toLowerCase() === searchText.toLowerCase() + ) + : options; + + setSortedOptions(sortMostRelevantSearch(filteredOptions, searchText)); + if (isZuid && filteredOptions?.length === 1) { + setSelectedParent(filteredOptions[0]); + } + }, [ + options, + searchText, + setSortedOptions, + sortMostRelevantSearch, + setSelectedParent, + ]); + return ( { > } renderOption={(props, value) => ( - + {value.text} )} @@ -244,10 +270,15 @@ export const ItemParent = ({ onChange }: ItemParentProps) => { }} onChange={(_, value) => { // Always default to homepage when no parent is selected - setSelectedParent( - value !== null ? value : { text: "/", value: "0" } - ); - onChange(value !== null ? value.value : "0", "parentZUID"); + setSelectedParent(value); + onChange(value?.value, "parentZUID"); + }} + onBlur={(event: FocusEvent) => { + // Always default to homepage when no parent is selected + if (!event?.target?.value) { + setSelectedParent({ text: "/", value: "0" }); + onChange("0", "parentZUID"); + } }} loading={isLoadingOptions} sx={{ diff --git a/src/shell/components/GlobalSearch/utils.ts b/src/shell/components/GlobalSearch/utils.ts index cb15feefd..96a66fdca 100644 --- a/src/shell/components/GlobalSearch/utils.ts +++ b/src/shell/components/GlobalSearch/utils.ts @@ -84,3 +84,51 @@ export const splitTextAndAccelerator = ( return { text, resourceType }; }; + +export interface SearchResult { + text: string; + value: string; +} + +export const sortMostRelevantSearch = ( + searchResults: SearchResult[], + searchString: string +): SearchResult[] => { + const normalize = (stringVal: string) => stringVal.replace(/^\/|\/$/g, ""); + + return searchResults.sort((a, b) => { + const normalizedSearch = normalize(searchString); + const normalizedA = normalize(a.text); + const normalizedB = normalize(b.text); + + // 1. Exact match (normalized) + const isExactMatchA = normalizedA === normalizedSearch; + const isExactMatchB = normalizedB === normalizedSearch; + + if (isExactMatchA && !isExactMatchB) return -1; + if (!isExactMatchA && isExactMatchB) return 1; + + // 2. URLs starting with the search string + const startsWithSearchA = normalizedA.startsWith(normalizedSearch); + const startsWithSearchB = normalizedB.startsWith(normalizedSearch); + + if (startsWithSearchA && !startsWithSearchB) return -1; + if (!startsWithSearchA && startsWithSearchB) return 1; + + // 3. URLs containing the search string + const containsSearchA = normalizedA.includes(normalizedSearch); + const containsSearchB = normalizedB.includes(normalizedSearch); + + if (containsSearchA && !containsSearchB) return -1; + if (!containsSearchA && containsSearchB) return 1; + + // 4. Compare by nested levels (fewer segments come first) + const nestedLevelA = a.text.split("/").filter(Boolean).length; + const nestedLevelB = b.text.split("/").filter(Boolean).length; + + if (nestedLevelA !== nestedLevelB) return nestedLevelA - nestedLevelB; + + // 5. Alphabetical order + return normalizedA.localeCompare(normalizedB); + }); +};