diff --git a/packages/admin/dashboard/src/components/common/thumbnail/thumbnail.tsx b/packages/admin/dashboard/src/components/common/thumbnail/thumbnail.tsx
index f439a2cd38..ffd0002f12 100644
--- a/packages/admin/dashboard/src/components/common/thumbnail/thumbnail.tsx
+++ b/packages/admin/dashboard/src/components/common/thumbnail/thumbnail.tsx
@@ -1,13 +1,23 @@
import { Photo } from "@medusajs/icons"
+import { clx } from "@medusajs/ui"
type ThumbnailProps = {
src?: string | null
alt?: string
+ size?: "small" | "base"
}
-export const Thumbnail = ({ src, alt }: ThumbnailProps) => {
+export const Thumbnail = ({ src, alt, size = "base" }: ThumbnailProps) => {
return (
-
+
{src ? (
{
+ const [area, setArea] = useState
("all")
+ const [search, setSearch] = useState("")
+ const [limit, setLimit] = useState(DEFAULT_SEARCH_LIMIT)
const { open, onOpenChange } = useSearch()
- const globalCommands = useGlobalShortcuts()
const location = useLocation()
const { t } = useTranslation()
const navigate = useNavigate()
+ const inputRef = useRef(null)
+ const listRef = useRef(null)
+
+ const { staticResults, dynamicResults, isFetching } = useSearchResults({
+ area,
+ limit,
+ q: search,
+ })
+
+ const handleReset = useCallback(() => {
+ setArea("all")
+ setSearch("")
+ setLimit(DEFAULT_SEARCH_LIMIT)
+ }, [setLimit])
+
+ const handleBack = () => {
+ handleReset()
+ inputRef.current?.focus()
+ }
+
+ const handleOpenChange = useCallback(
+ (open: boolean) => {
+ if (!open) {
+ handleReset()
+ }
+
+ onOpenChange(open)
+ },
+ [onOpenChange, handleReset]
+ )
+
useEffect(() => {
- onOpenChange(false)
- }, [location.pathname, onOpenChange])
+ handleOpenChange(false)
+ }, [location.pathname, handleOpenChange])
- const links = useMemo(() => {
- const groups = new Map()
+ const handleSelect = (item: { to?: string; callback?: () => void }) => {
+ handleOpenChange(false)
- globalCommands.forEach((command) => {
- const group = groups.get(command.type) || []
- group.push(command)
- groups.set(command.type, group)
- })
-
- return Array.from(groups).map(([title, items]) => ({
- title,
- items,
- }))
- }, [globalCommands])
-
- const handleSelect = (shortcut: Shortcut) => {
- onOpenChange(false)
-
- if (shortcut.to) {
- navigate(shortcut.to)
+ if (item.to) {
+ navigate(item.to)
return
}
- if (shortcut.callback) {
- shortcut.callback()
+ if (item.callback) {
+ item.callback()
return
}
}
+ const handleShowMore = (area: SearchArea) => {
+ if (area === "all") {
+ setLimit(DEFAULT_SEARCH_LIMIT)
+ } else {
+ setLimit(SEARCH_LIMIT_INCREMENT)
+ }
+ setArea(area)
+ inputRef.current?.focus()
+ }
+
+ const handleLoadMore = () => {
+ setLimit((l) => l + SEARCH_LIMIT_INCREMENT)
+ }
+
+ const filteredStaticResults = useMemo(() => {
+ const filteredResults: typeof staticResults = []
+
+ staticResults.forEach((group) => {
+ const filteredItems = matchSorter(group.items, search, {
+ keys: ["label"],
+ })
+
+ if (filteredItems.length === 0) {
+ return
+ }
+
+ filteredResults.push({
+ ...group,
+ items: filteredItems,
+ })
+ })
+
+ return filteredResults
+ }, [staticResults, search])
+
+ const handleSearch = (q: string) => {
+ setSearch(q)
+ listRef.current?.scrollTo({ top: 0 })
+ }
+
+ const showLoading = useMemo(() => {
+ return isFetching && !dynamicResults.length && !filteredStaticResults.length
+ }, [isFetching, dynamicResults, filteredStaticResults])
+
return (
-
-
-
- {t("general.noResultsTitle")}
- {links.map((group) => {
+
+
+
+ {showLoading && }
+ {dynamicResults.map((group) => {
+ return (
+
+ {group.items.map((item) => {
+ return (
+ handleSelect(item)}
+ value={item.value}
+ className="flex items-center justify-between"
+ >
+
+ {item.thumbnail && (
+
+ )}
+ {item.title}
+ {item.subtitle && (
+
+ {item.subtitle}
+
+ )}
+
+
+ )
+ })}
+ {group.hasMore && area === "all" && (
+ handleShowMore(group.area)}
+ hidden={true}
+ value={`${group.title}:show:more`} // Prevent the "Show more" buttons across groups from sharing the same value/state
+ >
+
+
+
+ {t("app.search.showMore")}
+
+
+
+ )}
+ {group.hasMore && area === group.area && (
+
+
+
+
+ {t("app.search.loadMore", {
+ count: Math.min(
+ SEARCH_LIMIT_INCREMENT,
+ group.count - limit
+ ),
+ })}
+
+
+
+ )}
+
+ )
+ })}
+ {filteredStaticResults.map((group) => {
return (
{
)
})}
+ {!showLoading && }
)
@@ -107,6 +274,7 @@ const CommandPalette = forwardRef<
ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
{
const { t } = useTranslation()
+ const preserveHeight = useMemo(() => {
+ return props.isLoading && Children.count(children) === 0
+ }, [props.isLoading, children])
+
return (
-
-
+
+
+ {t("app.search.title")}
+
+
+ {t("app.search.description")}
+
+
{children}
@@ -158,27 +345,117 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
const CommandInput = forwardRef<
ElementRef
,
- ComponentPropsWithoutRef
->(({ className, ...props }, ref) => {
- const { t } = useTranslation()
+ ComponentPropsWithoutRef & {
+ area: SearchArea
+ setArea: (area: SearchArea) => void
+ isFetching: boolean
+ onBack?: () => void
+ }
+>(
+ (
+ {
+ className,
+ value,
+ onValueChange,
+ area,
+ setArea,
+ isFetching,
+ onBack,
+ ...props
+ },
+ ref
+ ) => {
+ const { t } = useTranslation()
+ const innerRef = useRef(null)
- return (
-
-
- {/* TODO: Add filter once we have search engine */}
-
{t("app.search.allAreas")}
+ useImperativeHandle
(
+ ref,
+ () => innerRef.current
+ )
+
+ return (
+
+
+
+
+
+ {t(`app.search.groups.${area}`)}
+
+
+
+ {
+ e.preventDefault()
+ innerRef.current?.focus()
+ }}
+ >
+ setArea(v as SearchArea)}
+ >
+ {SEARCH_AREAS.map((area) => (
+
+ {area === "command" && }
+
+ {t(`app.search.groups.${area}`)}
+
+ {area === "all" && }
+
+ ))}
+
+
+
+
+
+ {onBack && (
+
+
+
+ )}
+
+
+ {isFetching && (
+
+ )}
+ {value && (
+ {
+ onValueChange?.("")
+ innerRef.current?.focus()
+ }}
+ >
+ {t("actions.clear")}
+
+ )}
+
+
-
-
- )
-})
+ )
+ }
+)
CommandInput.displayName = Command.Input.displayName
@@ -200,13 +477,58 @@ CommandList.displayName = Command.List.displayName
const CommandEmpty = forwardRef<
ElementRef
,
- ComponentPropsWithoutRef
->((props, ref) => (
-
-))
+ Omit, "children"> & {
+ q?: string
+ }
+>((props, ref) => {
+ const { t } = useTranslation()
+
+ return (
+
+
+
+
+
+ {props.q
+ ? t("app.search.noResultsTitle")
+ : t("app.search.emptySearchTitle")}
+
+
+ {props.q
+ ? t("app.search.noResultsMessage")
+ : t("app.search.emptySearchMessage")}
+
+
+
+
+ )
+})
CommandEmpty.displayName = Command.Empty.displayName
+const CommandLoading = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>((props, ref) => {
+ return (
+
+
+
+
+ {Array.from({ length: 7 }).map((_, index) => (
+
+
+
+ ))}
+
+ )
+})
+CommandLoading.displayName = Command.Loading.displayName
+
const CommandGroup = forwardRef<
ElementRef,
ComponentPropsWithoutRef
@@ -214,7 +536,7 @@ const CommandGroup = forwardRef<
{
+ const staticResults = useStaticSearchResults(area)
+ const { dynamicResults, isFetching } = useDynamicSearchResults(area, limit, q)
+
+ return {
+ staticResults,
+ dynamicResults,
+ isFetching,
+ }
+}
+
+const useStaticSearchResults = (currentArea: SearchArea) => {
+ const globalCommands = useGlobalShortcuts()
+
+ const results = useMemo(() => {
+ const groups = new Map()
+
+ globalCommands.forEach((command) => {
+ const group = groups.get(command.type) || []
+ group.push(command)
+ groups.set(command.type, group)
+ })
+
+ let filteredGroups: [ShortcutType, Shortcut[]][]
+
+ switch (currentArea) {
+ case "all":
+ filteredGroups = Array.from(groups)
+ break
+ case "navigation":
+ filteredGroups = Array.from(groups).filter(
+ ([type]) => type === "pageShortcut" || type === "settingShortcut"
+ )
+ break
+ case "command":
+ filteredGroups = Array.from(groups).filter(
+ ([type]) => type === "commandShortcut"
+ )
+ break
+ default:
+ filteredGroups = []
+ }
+
+ return filteredGroups.map(([title, items]) => ({
+ title,
+ items,
+ }))
+ }, [globalCommands, currentArea])
+
+ return results
+}
+
+const useDynamicSearchResults = (
+ currentArea: SearchArea,
+ limit: number,
+ q?: string
+) => {
+ const { t } = useTranslation()
+
+ const debouncedSearch = useDebouncedSearch(q, 300)
+
+ const orderResponse = useOrders(
+ {
+ q: debouncedSearch?.replace(/^#/, ""), // Since we display the ID with a # prefix, it's natural for the user to include it in the search. This will however cause no results to be returned, so we remove the # prefix from the search query.
+ limit,
+ fields: "id,display_id,email",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "order"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const productResponse = useProducts(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,title,thumbnail",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "product"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const productVariantResponse = useVariants(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,title,sku",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "productVariant"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const categoryResponse = useProductCategories(
+ {
+ // TODO: Remove the OR condition once the list endpoint does not throw when q equals an empty string
+ q: debouncedSearch || undefined,
+ limit,
+ fields: "id,name",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "category"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const collectionResponse = useCollections(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,title",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "collection"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const customerResponse = useCustomers(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,email,first_name,last_name",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "customer"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const customerGroupResponse = useCustomerGroups(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,name",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "customerGroup"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const inventoryResponse = useInventoryItems(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,title,sku",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "inventory"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const promotionResponse = usePromotions(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,code",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "promotion"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const campaignResponse = useCampaigns(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,name",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "campaign"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const priceListResponse = usePriceLists(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,title",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "priceList"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const userResponse = useUsers(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,email,first_name,last_name",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "user"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const regionResponse = useRegions(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,name",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "region"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const taxRegionResponse = useTaxRegions(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,country_code,province_code",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "taxRegion"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const returnReasonResponse = useReturnReasons(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,label,value",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "returnReason"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const salesChannelResponse = useSalesChannels(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,name",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "salesChannel"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const productTypeResponse = useProductTypes(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,value",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "productType"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const productTagResponse = useProductTags(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,value",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "productTag"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const locationResponse = useStockLocations(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,name",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "location"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const shippingProfileResponse = useShippingProfiles(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,name",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "shippingProfile"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const publishableApiKeyResponse = useApiKeys(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,title,redacted",
+ type: "publishable",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "publishableApiKey"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const secretApiKeyResponse = useApiKeys(
+ {
+ q: debouncedSearch,
+ limit,
+ fields: "id,title,redacted",
+ type: "secret",
+ },
+ {
+ enabled: isAreaEnabled(currentArea, "secretApiKey"),
+ placeholderData: keepPreviousData,
+ }
+ )
+
+ const responseMap = useMemo(
+ () => ({
+ order: orderResponse,
+ product: productResponse,
+ productVariant: productVariantResponse,
+ collection: collectionResponse,
+ category: categoryResponse,
+ inventory: inventoryResponse,
+ customer: customerResponse,
+ customerGroup: customerGroupResponse,
+ promotion: promotionResponse,
+ campaign: campaignResponse,
+ priceList: priceListResponse,
+ user: userResponse,
+ region: regionResponse,
+ taxRegion: taxRegionResponse,
+ returnReason: returnReasonResponse,
+ salesChannel: salesChannelResponse,
+ productType: productTypeResponse,
+ productTag: productTagResponse,
+ location: locationResponse,
+ shippingProfile: shippingProfileResponse,
+ publishableApiKey: publishableApiKeyResponse,
+ secretApiKey: secretApiKeyResponse,
+ }),
+ [
+ orderResponse,
+ productResponse,
+ productVariantResponse,
+ inventoryResponse,
+ categoryResponse,
+ collectionResponse,
+ customerResponse,
+ customerGroupResponse,
+ promotionResponse,
+ campaignResponse,
+ priceListResponse,
+ userResponse,
+ regionResponse,
+ taxRegionResponse,
+ returnReasonResponse,
+ salesChannelResponse,
+ productTypeResponse,
+ productTagResponse,
+ locationResponse,
+ shippingProfileResponse,
+ publishableApiKeyResponse,
+ secretApiKeyResponse,
+ ]
+ )
+
+ const results = useMemo(() => {
+ const groups = Object.entries(responseMap)
+ .map(([key, response]) => {
+ const area = key as SearchArea
+ if (isAreaEnabled(currentArea, area) || currentArea === "all") {
+ return transformDynamicSearchResults(area, limit, t, response)
+ }
+ return null
+ })
+ .filter(Boolean) // Remove null values
+
+ return groups
+ }, [responseMap, currentArea, limit, t])
+
+ const isAreaFetching = useCallback(
+ (area: SearchArea): boolean => {
+ if (area === "all") {
+ return Object.values(responseMap).some(
+ (response) => response.isFetching
+ )
+ }
+
+ return (
+ isAreaEnabled(currentArea, area) &&
+ responseMap[area as keyof typeof responseMap]?.isFetching
+ )
+ },
+ [currentArea, responseMap]
+ )
+
+ const isFetching = useMemo(() => {
+ return isAreaFetching(currentArea)
+ }, [currentArea, isAreaFetching])
+
+ const dynamicResults = q
+ ? (results.filter(
+ (group) => !!group && group.items.length > 0
+ ) as DynamicSearchResult[])
+ : []
+
+ return {
+ dynamicResults,
+ isFetching,
+ }
+}
+
+const useDebouncedSearch = (value: string | undefined, delay: number) => {
+ const [debouncedValue, setDebouncedValue] = useState(value)
+
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value)
+ }, delay)
+
+ return () => {
+ clearTimeout(handler)
+ }
+ }, [value, delay])
+
+ return debouncedValue
+}
+
+function isAreaEnabled(area: SearchArea, currentArea: SearchArea) {
+ if (area === "all") {
+ return true
+ }
+ if (area === currentArea) {
+ return true
+ }
+ return false
+}
+
+type TransformMap = {
+ [K in SearchArea]?: {
+ dataKey: string
+ transform: (item: any) => {
+ id: string
+ title: string
+ subtitle?: string
+ to: string
+ value: string
+ thumbnail?: string
+ }
+ }
+}
+
+const transformMap: TransformMap = {
+ order: {
+ dataKey: "orders",
+ transform: (order: HttpTypes.AdminOrder) => ({
+ id: order.id,
+ title: `#${order.display_id}`,
+ subtitle: order.email ?? undefined,
+ to: `/orders/${order.id}`,
+ value: `order:${order.id}`,
+ }),
+ },
+ product: {
+ dataKey: "products",
+ transform: (product: HttpTypes.AdminProduct) => ({
+ id: product.id,
+ title: product.title,
+ to: `/products/${product.id}`,
+ thumbnail: product.thumbnail ?? undefined,
+ value: `product:${product.id}`,
+ }),
+ },
+ productVariant: {
+ dataKey: "variants",
+ transform: (variant: HttpTypes.AdminProductVariant) => ({
+ id: variant.id,
+ title: variant.title!,
+ subtitle: variant.sku ?? undefined,
+ to: `/products/${variant.product_id}/variants/${variant.id}`,
+ value: `variant:${variant.id}`,
+ }),
+ },
+ category: {
+ dataKey: "product_categories",
+ transform: (category: HttpTypes.AdminProductCategory) => ({
+ id: category.id,
+ title: category.name,
+ to: `/categories/${category.id}`,
+ value: `category:${category.id}`,
+ }),
+ },
+ inventory: {
+ dataKey: "inventory_items",
+ transform: (inventory: HttpTypes.AdminInventoryItem) => ({
+ id: inventory.id,
+ title: inventory.title ?? "",
+ subtitle: inventory.sku ?? undefined,
+ to: `/inventory/${inventory.id}`,
+ value: `inventory:${inventory.id}`,
+ }),
+ },
+ customer: {
+ dataKey: "customers",
+ transform: (customer: HttpTypes.AdminCustomer) => {
+ const name = [customer.first_name, customer.last_name]
+ .filter(Boolean)
+ .join(" ")
+ return {
+ id: customer.id,
+ title: name || customer.email,
+ subtitle: name ? customer.email : undefined,
+ to: `/customers/${customer.id}`,
+ value: `customer:${customer.id}`,
+ }
+ },
+ },
+ customerGroup: {
+ dataKey: "customer_groups",
+ transform: (customerGroup: HttpTypes.AdminCustomerGroup) => ({
+ id: customerGroup.id,
+ title: customerGroup.name!,
+ to: `/customer-groups/${customerGroup.id}`,
+ value: `customerGroup:${customerGroup.id}`,
+ }),
+ },
+ collection: {
+ dataKey: "collections",
+ transform: (collection: HttpTypes.AdminCollection) => ({
+ id: collection.id,
+ title: collection.title,
+ to: `/collections/${collection.id}`,
+ value: `collection:${collection.id}`,
+ }),
+ },
+ promotion: {
+ dataKey: "promotions",
+ transform: (promotion: HttpTypes.AdminPromotion) => ({
+ id: promotion.id,
+ title: promotion.code!,
+ to: `/promotions/${promotion.id}`,
+ value: `promotion:${promotion.id}`,
+ }),
+ },
+ campaign: {
+ dataKey: "campaigns",
+ transform: (campaign: HttpTypes.AdminCampaign) => ({
+ id: campaign.id,
+ title: campaign.name,
+ to: `/campaigns/${campaign.id}`,
+ value: `campaign:${campaign.id}`,
+ }),
+ },
+ priceList: {
+ dataKey: "price_lists",
+ transform: (priceList: HttpTypes.AdminPriceList) => ({
+ id: priceList.id,
+ title: priceList.title,
+ to: `/price-lists/${priceList.id}`,
+ value: `priceList:${priceList.id}`,
+ }),
+ },
+ user: {
+ dataKey: "users",
+ transform: (user: HttpTypes.AdminUser) => ({
+ id: user.id,
+ title: `${user.first_name} ${user.last_name}`,
+ subtitle: user.email,
+ to: `/users/${user.id}`,
+ value: `user:${user.id}`,
+ }),
+ },
+ region: {
+ dataKey: "regions",
+ transform: (region: HttpTypes.AdminRegion) => ({
+ id: region.id,
+ title: region.name,
+ to: `/regions/${region.id}`,
+ value: `region:${region.id}`,
+ }),
+ },
+ taxRegion: {
+ dataKey: "tax_regions",
+ transform: (taxRegion: HttpTypes.AdminTaxRegion) => ({
+ id: taxRegion.id,
+ title:
+ taxRegion.province_code?.toUpperCase() ??
+ taxRegion.country_code!.toUpperCase(),
+ subtitle: taxRegion.province_code ? taxRegion.country_code! : undefined,
+ to: `/tax-regions/${taxRegion.id}`,
+ value: `taxRegion:${taxRegion.id}`,
+ }),
+ },
+ returnReason: {
+ dataKey: "return_reasons",
+ transform: (returnReason: HttpTypes.AdminReturnReason) => ({
+ id: returnReason.id,
+ title: returnReason.label,
+ subtitle: returnReason.value,
+ to: `/return-reasons/${returnReason.id}/edit`,
+ value: `returnReason:${returnReason.id}`,
+ }),
+ },
+ salesChannel: {
+ dataKey: "sales_channels",
+ transform: (salesChannel: HttpTypes.AdminSalesChannel) => ({
+ id: salesChannel.id,
+ title: salesChannel.name,
+ to: `/sales-channels/${salesChannel.id}`,
+ value: `salesChannel:${salesChannel.id}`,
+ }),
+ },
+ productType: {
+ dataKey: "product_types",
+ transform: (productType: HttpTypes.AdminProductType) => ({
+ id: productType.id,
+ title: productType.value,
+ to: `/product-types/${productType.id}`,
+ value: `productType:${productType.id}`,
+ }),
+ },
+ productTag: {
+ dataKey: "product_tags",
+ transform: (productTag: HttpTypes.AdminProductTag) => ({
+ id: productTag.id,
+ title: productTag.value,
+ to: `/product-tags/${productTag.id}`,
+ value: `productTag:${productTag.id}`,
+ }),
+ },
+ location: {
+ dataKey: "stock_locations",
+ transform: (location: HttpTypes.AdminStockLocation) => ({
+ id: location.id,
+ title: location.name,
+ to: `/locations/${location.id}`,
+ value: `location:${location.id}`,
+ }),
+ },
+ shippingProfile: {
+ dataKey: "shipping_profiles",
+ transform: (shippingProfile: HttpTypes.AdminShippingProfile) => ({
+ id: shippingProfile.id,
+ title: shippingProfile.name,
+ to: `/shipping-profiles/${shippingProfile.id}`,
+ value: `shippingProfile:${shippingProfile.id}`,
+ }),
+ },
+ publishableApiKey: {
+ dataKey: "api_keys",
+ transform: (apiKey: HttpTypes.AdminApiKeyResponse["api_key"]) => ({
+ id: apiKey.id,
+ title: apiKey.title,
+ subtitle: apiKey.redacted,
+ to: `/publishable-api-keys/${apiKey.id}`,
+ value: `publishableApiKey:${apiKey.id}`,
+ }),
+ },
+ secretApiKey: {
+ dataKey: "api_keys",
+ transform: (apiKey: HttpTypes.AdminApiKeyResponse["api_key"]) => ({
+ id: apiKey.id,
+ title: apiKey.title,
+ subtitle: apiKey.redacted,
+ to: `/secret-api-keys/${apiKey.id}`,
+ value: `secretApiKey:${apiKey.id}`,
+ }),
+ },
+}
+
+function transformDynamicSearchResults(
+ type: SearchArea,
+ limit: number,
+ t: TFunction,
+ response?: T
+): DynamicSearchResult | undefined {
+ if (!response || !transformMap[type]) {
+ return undefined
+ }
+
+ const { dataKey, transform } = transformMap[type]!
+ const data = response[dataKey as keyof T]
+
+ if (!data || !Array.isArray(data)) {
+ return undefined
+ }
+
+ return {
+ title: t(`app.search.groups.${type}`),
+ area: type,
+ hasMore: response.count > limit,
+ count: response.count,
+ items: data.map(transform),
+ }
+}
diff --git a/packages/admin/dashboard/src/hooks/api/orders.tsx b/packages/admin/dashboard/src/hooks/api/orders.tsx
index 9dd92cef17..b76c0af3fa 100644
--- a/packages/admin/dashboard/src/hooks/api/orders.tsx
+++ b/packages/admin/dashboard/src/hooks/api/orders.tsx
@@ -71,9 +71,14 @@ export const useOrderPreview = (
}
export const useOrders = (
- query?: Record,
+ query?: HttpTypes.AdminOrderFilters,
options?: Omit<
- UseQueryOptions,
+ UseQueryOptions<
+ HttpTypes.AdminOrderListResponse,
+ FetchError,
+ HttpTypes.AdminOrderListResponse,
+ QueryKey
+ >,
"queryFn" | "queryKey"
>
) => {
diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json
index bf926aa1b4..103515072d 100644
--- a/packages/admin/dashboard/src/i18n/translations/en.json
+++ b/packages/admin/dashboard/src/i18n/translations/en.json
@@ -124,6 +124,7 @@
"edit": "Edit",
"addItems": "Add items",
"download": "Download",
+ "clear": "Clear",
"clearAll": "Clear all",
"apply": "Apply",
"add": "Add",
@@ -140,10 +141,46 @@
"app": {
"search": {
"label": "Search",
+ "title": "Search",
+ "description": "Search your entire store, including orders, products, customers, and more.",
"allAreas": "All areas",
"navigation": "Navigation",
"openResult": "Open result",
- "placeholder": "Jump to or find anything..."
+ "showMore": "Show more",
+ "placeholder": "Jump to or find anything...",
+ "noResultsTitle": "No results found",
+ "noResultsMessage": "We couldn't find anything that matched your search.",
+ "emptySearchTitle": "Type to search",
+ "emptySearchMessage": "Enter a keyword or phrase to explore.",
+ "loadMore": "Load {{count}} more",
+ "groups": {
+ "all": "All areas",
+ "customer": "Customers",
+ "customerGroup": "Customer Groups",
+ "product": "Products",
+ "productVariant": "Product Variants",
+ "inventory": "Inventory",
+ "reservation": "Reservations",
+ "category": "Categories",
+ "collection": "Collections",
+ "order": "Orders",
+ "promotion": "Promotions",
+ "campaign": "Campaigns",
+ "priceList": "Price Lists",
+ "user": "Users",
+ "region": "Regions",
+ "taxRegion": "Tax Regions",
+ "returnReason": "Return Reasons",
+ "salesChannel": "Sales Channels",
+ "productType": "Product Types",
+ "productTag": "Product Tags",
+ "location": "Locations",
+ "shippingProfile": "Shipping Profiles",
+ "publishableApiKey": "Publishable API Keys",
+ "secretApiKey": "Secret API Keys",
+ "command": "Commands",
+ "navigation": "Navigation"
+ }
},
"keyboardShortcuts": {
"pageShortcut": "Jump to",
diff --git a/packages/admin/dashboard/src/index.css b/packages/admin/dashboard/src/index.css
index bc87c9f0be..e73e75abb6 100644
--- a/packages/admin/dashboard/src/index.css
+++ b/packages/admin/dashboard/src/index.css
@@ -30,6 +30,7 @@
:root {
@apply bg-ui-bg-subtle text-ui-fg-base antialiased;
text-rendering: optimizeLegibility;
+ color-scheme: light dark;
}
}
diff --git a/packages/core/types/src/http/customer/admin/responses.ts b/packages/core/types/src/http/customer/admin/responses.ts
index 253dc6b81c..2844a27c7c 100644
--- a/packages/core/types/src/http/customer/admin/responses.ts
+++ b/packages/core/types/src/http/customer/admin/responses.ts
@@ -10,7 +10,7 @@ export interface AdminCustomerResponse {
}
export type AdminCustomerListResponse = PaginatedResponse<{
- customers: AdminCustomer
+ customers: AdminCustomer[]
}>
export interface AdminCustomerAddressResponse {