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 && ( + + )} +
+ ) + })} + {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 && ( + + )} +
+
- -
- ) -}) + ) + } +) 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 {