From 843351cbd049047c36f1c81e4e36461729e7a5b3 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Thu, 20 Mar 2025 11:46:51 +0100 Subject: [PATCH 01/15] add variant search by SKU --- .../NavigatorSearch/modes/catalog.ts | 21 ++++++++++++++++--- .../NavigatorSearch/modes/messages.ts | 4 ++++ .../NavigatorSearch/queries/queries.ts | 18 ++++++++++++++++ src/graphql/hooks.generated.ts | 17 +++++++++++++++ src/graphql/types.generated.ts | 2 +- 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/components/NavigatorSearch/modes/catalog.ts b/src/components/NavigatorSearch/modes/catalog.ts index 8a1171eb146..e6aacfe0f9a 100644 --- a/src/components/NavigatorSearch/modes/catalog.ts +++ b/src/components/NavigatorSearch/modes/catalog.ts @@ -4,7 +4,7 @@ import { collectionUrl } from "@dashboard/collections/urls"; import { SearchCatalogQuery } from "@dashboard/graphql"; import { UseNavigatorResult } from "@dashboard/hooks/useNavigator"; import { fuzzySearch } from "@dashboard/misc"; -import { productUrl } from "@dashboard/products/urls"; +import { productUrl, productVariantEditUrl } from "@dashboard/products/urls"; import { mapEdgesToItems } from "@dashboard/utils/maps"; import { IntlShape } from "react-intl"; @@ -57,10 +57,25 @@ export function searchInCatalog( text: product.name, type: "catalog", })); + const variants: QuickSearchActionInput[] = ( + mapEdgesToItems(catalog?.productVariants) || [] + ).map(variant => ({ + caption: intl.formatMessage(messages.variant), + extraInfo: variant.product.category.name, + label: `${variant.product.name} / ${variant.name} (${variant.sku})`, + onClick: () => { + navigate(productVariantEditUrl(variant.product.id, variant.id)); + + return false; + }, + text: variant.name, + type: "catalog", + })); - const searchableItems = [...categories, ...collections, ...products]; + const searchableItems = [...categories, ...collections, ...products, ...variants]; + const searchResults = fuzzySearch(searchableItems, search, ["label"]); - return fuzzySearch(searchableItems, search, ["label"]); + return searchResults; } function getCatalogModeActions( diff --git a/src/components/NavigatorSearch/modes/messages.ts b/src/components/NavigatorSearch/modes/messages.ts index b352d60fc70..493c15fe49b 100644 --- a/src/components/NavigatorSearch/modes/messages.ts +++ b/src/components/NavigatorSearch/modes/messages.ts @@ -101,6 +101,10 @@ const messages = defineMessages({ id: "x/ZVlU", defaultMessage: "Product", }, + variant: { + defaultMessage: "Product variant", + id: "GC8T3V", + }, }); export default messages; diff --git a/src/components/NavigatorSearch/queries/queries.ts b/src/components/NavigatorSearch/queries/queries.ts index b9fb6d1f85a..6a1129c8630 100644 --- a/src/components/NavigatorSearch/queries/queries.ts +++ b/src/components/NavigatorSearch/queries/queries.ts @@ -45,5 +45,23 @@ export const searchCatalog = gql` } } } + + productVariants(first: $first, filter: { search: $query }) { + edges { + node { + id + name + sku + product { + id + name + category { + id + name + } + } + } + } + } } `; diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 84571a6ba9e..578369f1eda 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -7033,6 +7033,23 @@ export const SearchCatalogDocument = gql` } } } + productVariants(first: $first, filter: {search: $query}) { + edges { + node { + id + name + sku + product { + id + name + category { + id + name + } + } + } + } + } } ${CollectionFragmentDoc}`; diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 163d7d6298e..66a78d54060 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -9588,7 +9588,7 @@ export type SearchCatalogQueryVariables = Exact<{ }>; -export type SearchCatalogQuery = { __typename: 'Query', categories: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string } }> } | null, collections: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, channelListings: Array<{ __typename: 'CollectionChannelListing', isPublished: boolean, publishedAt: any | null, channel: { __typename: 'Channel', id: string, name: string } }> | null } }> } | null, products: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, category: { __typename: 'Category', id: string, name: string } | null } }> } | null }; +export type SearchCatalogQuery = { __typename: 'Query', categories: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string } }> } | null, collections: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, channelListings: Array<{ __typename: 'CollectionChannelListing', isPublished: boolean, publishedAt: any | null, channel: { __typename: 'Channel', id: string, name: string } }> | null } }> } | null, products: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, category: { __typename: 'Category', id: string, name: string } | null } }> } | null, productVariants: { __typename: 'ProductVariantCountableConnection', edges: Array<{ __typename: 'ProductVariantCountableEdge', node: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, product: { __typename: 'Product', id: string, name: string, category: { __typename: 'Category', id: string, name: string } | null } } }> } | null }; export type ShopInfoQueryVariables = Exact<{ [key: string]: never; }>; From a4325123d5d76f155b8c1d89f12fd44fcbe90a52 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Thu, 20 Mar 2025 14:54:40 +0100 Subject: [PATCH 02/15] show media if available --- .../NavigatorSearch/NavigatorSearch.tsx | 2 +- .../NavigatorSearchSection.tsx | 37 +++++++++++------- .../NavigatorSearch/NavigatorThumbnail.tsx | 39 +++++++++++++++++++ .../NavigatorSearch/modes/catalog.ts | 5 +++ .../NavigatorSearch/modes/messages.ts | 4 ++ .../NavigatorSearch/queries/queries.ts | 17 ++++++++ src/components/NavigatorSearch/types.ts | 4 ++ src/fragments/quickSearchMedia.ts | 1 + src/graphql/hooks.generated.ts | 17 ++++++++ src/graphql/types.generated.ts | 2 +- 10 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 src/components/NavigatorSearch/NavigatorThumbnail.tsx create mode 100644 src/fragments/quickSearchMedia.ts diff --git a/src/components/NavigatorSearch/NavigatorSearch.tsx b/src/components/NavigatorSearch/NavigatorSearch.tsx index d68594e456d..f01d487b5e0 100644 --- a/src/components/NavigatorSearch/NavigatorSearch.tsx +++ b/src/components/NavigatorSearch/NavigatorSearch.tsx @@ -140,7 +140,7 @@ const NavigatorSearch: React.FC = () => { return ( - + (item ? item.label : "")} diff --git a/src/components/NavigatorSearch/NavigatorSearchSection.tsx b/src/components/NavigatorSearch/NavigatorSearchSection.tsx index 43ed8196958..43f80d53dcb 100644 --- a/src/components/NavigatorSearch/NavigatorSearchSection.tsx +++ b/src/components/NavigatorSearch/NavigatorSearchSection.tsx @@ -2,6 +2,7 @@ import { Box, Text } from "@saleor/macaw-ui-next"; import { GetItemPropsOptions } from "downshift"; import React from "react"; +import { NavigatorThumbnail } from "./NavigatorThumbnail"; import { QuickSearchAction } from "./types"; interface NavigatorSearchSectionProps { @@ -45,26 +46,34 @@ const NavigatorSearchSection: React.FC = props => { selected={highlightedIndex === index} key={[item.label, item.type].join(":")} cursor="pointer" + display="flex" + flexDirection="row" > + {item.thumbnail && ( + + )} + - {item.symbol && ( - - {item.symbol} - - )} + + {item.symbol && ( + + {item.symbol} + + )} - {item.label} + {item.label} - {item.caption && ( - - {item.caption} - - )} - + {item.caption && ( + + {item.caption} + + )} + - + - {item.extraInfo} + {item.extraInfo} + ); })} diff --git a/src/components/NavigatorSearch/NavigatorThumbnail.tsx b/src/components/NavigatorSearch/NavigatorThumbnail.tsx new file mode 100644 index 00000000000..47ec24aebfa --- /dev/null +++ b/src/components/NavigatorSearch/NavigatorThumbnail.tsx @@ -0,0 +1,39 @@ +import { Box } from "@saleor/macaw-ui-next"; +import React from "react"; + +const defaultProps = { + __height: "48px", + __width: "48px", +}; + +export const NavigatorThumbnail = ({ + src, + alt, +}: { + src: string | undefined; + alt: string | undefined; +}) => ( + + + +); diff --git a/src/components/NavigatorSearch/modes/catalog.ts b/src/components/NavigatorSearch/modes/catalog.ts index e6aacfe0f9a..80a2c6a3a8e 100644 --- a/src/components/NavigatorSearch/modes/catalog.ts +++ b/src/components/NavigatorSearch/modes/catalog.ts @@ -29,6 +29,8 @@ export function searchInCatalog( }, text: category.name, type: "catalog", + thumbnail: category.backgroundImage, + extraInfo: category.level === 0 ? intl.formatMessage(messages.root) : undefined, })); const collections: QuickSearchActionInput[] = ( mapEdgesToItems(catalog?.collections) || [] @@ -42,6 +44,7 @@ export function searchInCatalog( }, text: collection.name, type: "catalog", + thumbnail: collection.backgroundImage, })); const products: QuickSearchActionInput[] = ( mapEdgesToItems(catalog?.products) || [] @@ -56,6 +59,7 @@ export function searchInCatalog( }, text: product.name, type: "catalog", + thumbnail: product.thumbnail, })); const variants: QuickSearchActionInput[] = ( mapEdgesToItems(catalog?.productVariants) || [] @@ -70,6 +74,7 @@ export function searchInCatalog( }, text: variant.name, type: "catalog", + thumbnail: variant.product.thumbnail, })); const searchableItems = [...categories, ...collections, ...products, ...variants]; diff --git a/src/components/NavigatorSearch/modes/messages.ts b/src/components/NavigatorSearch/modes/messages.ts index 493c15fe49b..b976fed868e 100644 --- a/src/components/NavigatorSearch/modes/messages.ts +++ b/src/components/NavigatorSearch/modes/messages.ts @@ -101,6 +101,10 @@ const messages = defineMessages({ id: "x/ZVlU", defaultMessage: "Product", }, + root: { + defaultMessage: "This is root category", + id: "6iaXL/", + }, variant: { defaultMessage: "Product variant", id: "GC8T3V", diff --git a/src/components/NavigatorSearch/queries/queries.ts b/src/components/NavigatorSearch/queries/queries.ts index 6a1129c8630..9bedbbb5382 100644 --- a/src/components/NavigatorSearch/queries/queries.ts +++ b/src/components/NavigatorSearch/queries/queries.ts @@ -21,6 +21,11 @@ export const searchCatalog = gql` node { id name + backgroundImage(size: 64) { + url + alt + } + level } } } @@ -29,6 +34,10 @@ export const searchCatalog = gql` edges { node { ...Collection + backgroundImage(size: 64) { + url + alt + } } } } @@ -42,6 +51,10 @@ export const searchCatalog = gql` name } name + thumbnail(size: 64) { + alt + url + } } } } @@ -59,6 +72,10 @@ export const searchCatalog = gql` id name } + thumbnail(size: 64) { + alt + url + } } } } diff --git a/src/components/NavigatorSearch/types.ts b/src/components/NavigatorSearch/types.ts index 99dcd042f33..4f9116e3c58 100644 --- a/src/components/NavigatorSearch/types.ts +++ b/src/components/NavigatorSearch/types.ts @@ -6,6 +6,10 @@ export interface QuickSearchAction { label: string; price?: number; symbol?: string; + thumbnail?: { + alt: string; + url: string; + }; type: QuickSearchActionType; onClick: () => boolean; } diff --git a/src/fragments/quickSearchMedia.ts b/src/fragments/quickSearchMedia.ts new file mode 100644 index 00000000000..9ccc2768e97 --- /dev/null +++ b/src/fragments/quickSearchMedia.ts @@ -0,0 +1 @@ +import { gql } from "@apollo/client"; diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index 578369f1eda..cb9694fe3a9 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -7011,6 +7011,11 @@ export const SearchCatalogDocument = gql` node { id name + backgroundImage(size: 64) { + url + alt + } + level } } } @@ -7018,6 +7023,10 @@ export const SearchCatalogDocument = gql` edges { node { ...Collection + backgroundImage(size: 64) { + url + alt + } } } } @@ -7030,6 +7039,10 @@ export const SearchCatalogDocument = gql` name } name + thumbnail(size: 64) { + alt + url + } } } } @@ -7046,6 +7059,10 @@ export const SearchCatalogDocument = gql` id name } + thumbnail(size: 64) { + alt + url + } } } } diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 66a78d54060..3e309015ec9 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -9588,7 +9588,7 @@ export type SearchCatalogQueryVariables = Exact<{ }>; -export type SearchCatalogQuery = { __typename: 'Query', categories: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string } }> } | null, collections: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, channelListings: Array<{ __typename: 'CollectionChannelListing', isPublished: boolean, publishedAt: any | null, channel: { __typename: 'Channel', id: string, name: string } }> | null } }> } | null, products: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, category: { __typename: 'Category', id: string, name: string } | null } }> } | null, productVariants: { __typename: 'ProductVariantCountableConnection', edges: Array<{ __typename: 'ProductVariantCountableEdge', node: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, product: { __typename: 'Product', id: string, name: string, category: { __typename: 'Category', id: string, name: string } | null } } }> } | null }; +export type SearchCatalogQuery = { __typename: 'Query', categories: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, level: number, backgroundImage: { __typename: 'Image', url: string, alt: string | null } | null } }> } | null, collections: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, backgroundImage: { __typename: 'Image', url: string, alt: string | null } | null, channelListings: Array<{ __typename: 'CollectionChannelListing', isPublished: boolean, publishedAt: any | null, channel: { __typename: 'Channel', id: string, name: string } }> | null } }> } | null, products: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, category: { __typename: 'Category', id: string, name: string } | null, thumbnail: { __typename: 'Image', alt: string | null, url: string } | null } }> } | null, productVariants: { __typename: 'ProductVariantCountableConnection', edges: Array<{ __typename: 'ProductVariantCountableEdge', node: { __typename: 'ProductVariant', id: string, name: string, sku: string | null, product: { __typename: 'Product', id: string, name: string, category: { __typename: 'Category', id: string, name: string } | null, thumbnail: { __typename: 'Image', alt: string | null, url: string } | null } } }> } | null }; export type ShopInfoQueryVariables = Exact<{ [key: string]: never; }>; From 918c881a5e33daaf90e9e4e4cc1e8d54c8555560 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Thu, 20 Mar 2025 15:11:01 +0100 Subject: [PATCH 03/15] changeset + json messages --- .changeset/sharp-kangaroos-retire.md | 5 +++++ locale/defaultMessages.json | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/sharp-kangaroos-retire.md diff --git a/.changeset/sharp-kangaroos-retire.md b/.changeset/sharp-kangaroos-retire.md new file mode 100644 index 00000000000..7477bb87a87 --- /dev/null +++ b/.changeset/sharp-kangaroos-retire.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +In quick search you can now search for product variants with SKU. This means product variants are a new item added to the search catalogue. Catalogue items now show their media, if available. Category item display message if they don't have parents. diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index e5ce4afddcd..8102dd89d70 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1389,6 +1389,9 @@ "context": "tooltip, submit form", "string": "Transaction is non-refundable. You have to send refund manually." }, + "6iaXL/": { + "string": "This is root category" + }, "6iw4VR": { "context": "delete variant dialog title", "string": "Delete Product Variants" @@ -2832,6 +2835,9 @@ "context": "value input label", "string": "Discount value" }, + "GC8T3V": { + "string": "Product variant" + }, "GCPzKf": { "context": "ProductTypeDeleteWarningDialog single assigned items button label", "string": "View products" From 82b9fd13d58bf99dd29ffb34f18f2598a050c1dc Mon Sep 17 00:00:00 2001 From: Wojciech Date: Thu, 20 Mar 2025 15:40:22 +0100 Subject: [PATCH 04/15] add test --- package.json | 2 +- .../NavigatorSearch/modes/catalog.test.ts | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/components/NavigatorSearch/modes/catalog.test.ts diff --git a/package.json b/package.json index 5e951151463..84919c661b4 100644 --- a/package.json +++ b/package.json @@ -266,7 +266,7 @@ "prestart": "npm run build-types", "test": "jest src/", "test:ci": "jest src/ --coverage", - "test:watch": "jest --watch src/", + "test:watch": "jest --watch catalog.test.ts", "lint": "eslint \"{src,playwright}/**/*.@(tsx|ts|jsx|js)\" --fix", "lint:check-progress": "eslint-nibble \"src/**/*.@(tsx|ts|jsx|js)\"", "prebuild": "npm run build-types", diff --git a/src/components/NavigatorSearch/modes/catalog.test.ts b/src/components/NavigatorSearch/modes/catalog.test.ts new file mode 100644 index 00000000000..4e2223d6f8a --- /dev/null +++ b/src/components/NavigatorSearch/modes/catalog.test.ts @@ -0,0 +1,84 @@ +import { SearchCatalogQuery } from "@dashboard/graphql"; +import { IntlShape } from "react-intl"; + +import { searchInCatalog } from "./catalog"; + +describe("searchInCatalog", () => { + const intl = { + formatMessage: ({ defaultMessage }) => defaultMessage, + } as IntlShape; + const navigate = jest.fn(); + + it("should return variants if search is a sku", () => { + const mockCatalog: SearchCatalogQuery = { + productVariants: { + edges: [ + { + node: { + id: "1", + name: "Small", + sku: "PROD-SMALL", + product: { + id: "prod1", + name: "T-Shirt", + thumbnail: null, + category: { + name: "Apparel", + }, + }, + }, + }, + ], + }, + } as SearchCatalogQuery; + + const results = searchInCatalog("PROD-SMALL", intl, navigate, mockCatalog); + + expect(results).toHaveLength(1); + expect(results[0].label).toContain("PROD-SMALL"); + }); + + it("should return both product and its variant", () => { + const mockCatalog: SearchCatalogQuery = { + products: { + edges: [ + { + node: { + id: "prod1", + name: "T-Shirt", + thumbnail: null, + category: { + name: "Apparel", + }, + }, + }, + ], + }, + productVariants: { + edges: [ + { + node: { + id: "1", + name: "Small", + sku: "PROD-SMALL", + product: { + id: "prod1", + name: "T-Shirt", + thumbnail: null, + category: { + name: "Apparel", + }, + }, + }, + }, + ], + }, + } as SearchCatalogQuery; + + const results = searchInCatalog("T-Shirt", intl, navigate, mockCatalog); + + expect(results).toHaveLength(2); + expect(results[0].label).toBe("T-Shirt"); + expect(results[1].label).toBe("T-Shirt / Small (PROD-SMALL)"); + }); +}); From faec660a3dfd46e6b29944971a1aaeb84d268f04 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Thu, 20 Mar 2025 16:09:53 +0100 Subject: [PATCH 05/15] cleanup --- src/fragments/quickSearchMedia.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/fragments/quickSearchMedia.ts diff --git a/src/fragments/quickSearchMedia.ts b/src/fragments/quickSearchMedia.ts deleted file mode 100644 index 9ccc2768e97..00000000000 --- a/src/fragments/quickSearchMedia.ts +++ /dev/null @@ -1 +0,0 @@ -import { gql } from "@apollo/client"; From dbe2cd34e9e96258b34e02b4f90265eef9740c2b Mon Sep 17 00:00:00 2001 From: Wojciech Date: Thu, 20 Mar 2025 16:47:38 +0100 Subject: [PATCH 06/15] add loading state to thumbnails --- .../NavigatorSearchSection.tsx | 7 +- .../NavigatorSearch/NavigatorThumbnail.tsx | 83 +++++++++++++------ 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/components/NavigatorSearch/NavigatorSearchSection.tsx b/src/components/NavigatorSearch/NavigatorSearchSection.tsx index 43f80d53dcb..d7c2e0dc151 100644 --- a/src/components/NavigatorSearch/NavigatorSearchSection.tsx +++ b/src/components/NavigatorSearch/NavigatorSearchSection.tsx @@ -29,6 +29,9 @@ const NavigatorSearchSection: React.FC = props => { item, }); + // 'thumbnail' is 'null' when the item is from Saleor API, 'undefined' if it's nav item + const shouldRenderThumbnail = typeof item.thumbnail !== "undefined"; + return ( = props => { display="flex" flexDirection="row" > - {item.thumbnail && ( - + {shouldRenderThumbnail && ( + )} diff --git a/src/components/NavigatorSearch/NavigatorThumbnail.tsx b/src/components/NavigatorSearch/NavigatorThumbnail.tsx index 47ec24aebfa..a3a07b1599a 100644 --- a/src/components/NavigatorSearch/NavigatorThumbnail.tsx +++ b/src/components/NavigatorSearch/NavigatorThumbnail.tsx @@ -1,9 +1,10 @@ -import { Box } from "@saleor/macaw-ui-next"; -import React from "react"; +import { Box, Skeleton } from "@saleor/macaw-ui-next"; +import React, { useState } from "react"; const defaultProps = { __height: "48px", __width: "48px", + __minWidth: "48px", }; export const NavigatorThumbnail = ({ @@ -12,28 +13,58 @@ export const NavigatorThumbnail = ({ }: { src: string | undefined; alt: string | undefined; -}) => ( - +}) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false || !src); + + if (error) { + return ( + + ); + } + + return ( - -); + display="flex" + alignItems="center" + justifyContent="center" + __padding="2px" + borderRadius={4} + borderStyle="solid" + borderWidth={1} + borderColor="default1" + overflow="hidden" + marginRight={2} + position="relative" + {...defaultProps} + > + {loading && ( + + )} + { + setError(true); + }} + onLoad={() => { + setLoading(false); + }} + /> + + ); +}; From a5b17e2f0fc3c36e2b48f8fc061f02f8a198e0e8 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Fri, 21 Mar 2025 11:23:48 +0100 Subject: [PATCH 07/15] small UI changes --- package.json | 2 +- .../NavigatorSearch/NavigatorThumbnail.tsx | 6 +++--- .../NavigatorSearch/modes/catalog.test.ts | 4 ++-- src/components/NavigatorSearch/modes/catalog.ts | 9 +++++++-- src/components/NavigatorSearch/modes/labels.tsx | 16 ++++++++++++++++ src/components/NavigatorSearch/modes/messages.ts | 4 ++-- src/components/NavigatorSearch/types.ts | 5 ++++- 7 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 src/components/NavigatorSearch/modes/labels.tsx diff --git a/package.json b/package.json index 84919c661b4..718fee7b269 100644 --- a/package.json +++ b/package.json @@ -266,7 +266,7 @@ "prestart": "npm run build-types", "test": "jest src/", "test:ci": "jest src/ --coverage", - "test:watch": "jest --watch catalog.test.ts", + "test:watch": "jest --watch src", "lint": "eslint \"{src,playwright}/**/*.@(tsx|ts|jsx|js)\" --fix", "lint:check-progress": "eslint-nibble \"src/**/*.@(tsx|ts|jsx|js)\"", "prebuild": "npm run build-types", diff --git a/src/components/NavigatorSearch/NavigatorThumbnail.tsx b/src/components/NavigatorSearch/NavigatorThumbnail.tsx index a3a07b1599a..4e2e1a11080 100644 --- a/src/components/NavigatorSearch/NavigatorThumbnail.tsx +++ b/src/components/NavigatorSearch/NavigatorThumbnail.tsx @@ -2,9 +2,9 @@ import { Box, Skeleton } from "@saleor/macaw-ui-next"; import React, { useState } from "react"; const defaultProps = { - __height: "48px", - __width: "48px", - __minWidth: "48px", + __height: "40px", + __width: "40px", + __minWidth: "40px", }; export const NavigatorThumbnail = ({ diff --git a/src/components/NavigatorSearch/modes/catalog.test.ts b/src/components/NavigatorSearch/modes/catalog.test.ts index 4e2223d6f8a..b65d7607b4f 100644 --- a/src/components/NavigatorSearch/modes/catalog.test.ts +++ b/src/components/NavigatorSearch/modes/catalog.test.ts @@ -35,7 +35,7 @@ describe("searchInCatalog", () => { const results = searchInCatalog("PROD-SMALL", intl, navigate, mockCatalog); expect(results).toHaveLength(1); - expect(results[0].label).toContain("PROD-SMALL"); + expect(results[0].searchValue).toContain("PROD-SMALL"); }); it("should return both product and its variant", () => { @@ -79,6 +79,6 @@ describe("searchInCatalog", () => { expect(results).toHaveLength(2); expect(results[0].label).toBe("T-Shirt"); - expect(results[1].label).toBe("T-Shirt / Small (PROD-SMALL)"); + expect(results[1].searchValue).toContain("PROD-SMALL"); }); }); diff --git a/src/components/NavigatorSearch/modes/catalog.ts b/src/components/NavigatorSearch/modes/catalog.ts index 80a2c6a3a8e..992922bb5ae 100644 --- a/src/components/NavigatorSearch/modes/catalog.ts +++ b/src/components/NavigatorSearch/modes/catalog.ts @@ -9,6 +9,7 @@ import { mapEdgesToItems } from "@dashboard/utils/maps"; import { IntlShape } from "react-intl"; import { QuickSearchAction, QuickSearchActionInput } from "../types"; +import { getProductVariantLabel } from "./labels"; import messages from "./messages"; export function searchInCatalog( @@ -22,6 +23,7 @@ export function searchInCatalog( ).map(category => ({ caption: intl.formatMessage(messages.category), label: category.name, + searchValue: category.name, onClick: () => { navigate(categoryUrl(category.id)); @@ -37,6 +39,7 @@ export function searchInCatalog( ).map(collection => ({ caption: intl.formatMessage(messages.collection), label: collection.name, + searchValue: collection.name, onClick: () => { navigate(collectionUrl(collection.id)); @@ -52,6 +55,7 @@ export function searchInCatalog( caption: intl.formatMessage(messages.product), extraInfo: product.category.name, label: product.name, + searchValue: product.name, onClick: () => { navigate(productUrl(product.id)); @@ -66,7 +70,8 @@ export function searchInCatalog( ).map(variant => ({ caption: intl.formatMessage(messages.variant), extraInfo: variant.product.category.name, - label: `${variant.product.name} / ${variant.name} (${variant.sku})`, + label: getProductVariantLabel(variant), + searchValue: `${variant.product.name} ${variant.name} ${variant.sku}`, onClick: () => { navigate(productVariantEditUrl(variant.product.id, variant.id)); @@ -78,7 +83,7 @@ export function searchInCatalog( })); const searchableItems = [...categories, ...collections, ...products, ...variants]; - const searchResults = fuzzySearch(searchableItems, search, ["label"]); + const searchResults = fuzzySearch(searchableItems, search, ["searchValue"]); return searchResults; } diff --git a/src/components/NavigatorSearch/modes/labels.tsx b/src/components/NavigatorSearch/modes/labels.tsx new file mode 100644 index 00000000000..13391712e24 --- /dev/null +++ b/src/components/NavigatorSearch/modes/labels.tsx @@ -0,0 +1,16 @@ +import { SearchCatalogQuery } from "@dashboard/graphql"; +import { Text } from "@saleor/macaw-ui-next"; +import React from "react"; + +type Variant = NonNullable["edges"][0]["node"]; + +export const getProductVariantLabel = (variant: Variant) => { + return ( + <> + {variant.product.name} / {variant.name} + + ({variant.sku}) + + + ); +}; diff --git a/src/components/NavigatorSearch/modes/messages.ts b/src/components/NavigatorSearch/modes/messages.ts index b976fed868e..0e9040a0a06 100644 --- a/src/components/NavigatorSearch/modes/messages.ts +++ b/src/components/NavigatorSearch/modes/messages.ts @@ -106,8 +106,8 @@ const messages = defineMessages({ id: "6iaXL/", }, variant: { - defaultMessage: "Product variant", - id: "GC8T3V", + defaultMessage: "Variant", + id: "OK5+Fh", }, }); diff --git a/src/components/NavigatorSearch/types.ts b/src/components/NavigatorSearch/types.ts index 4f9116e3c58..4ece3fd5d1c 100644 --- a/src/components/NavigatorSearch/types.ts +++ b/src/components/NavigatorSearch/types.ts @@ -1,9 +1,12 @@ +import { ReactNode } from "react"; + export type QuickSearchActionType = "action" | "catalog" | "customer" | "view"; export interface QuickSearchAction { caption?: string; extraInfo?: string; - label: string; + label: string | ReactNode; + searchValue: string; price?: number; symbol?: string; thumbnail?: { From 2960555795ff0b09cf4472c3f661259a4cbdd862 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Fri, 21 Mar 2025 11:38:51 +0100 Subject: [PATCH 08/15] properly hide sku --- src/components/NavigatorSearch/modes/labels.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/NavigatorSearch/modes/labels.tsx b/src/components/NavigatorSearch/modes/labels.tsx index 13391712e24..b1f7fa6d424 100644 --- a/src/components/NavigatorSearch/modes/labels.tsx +++ b/src/components/NavigatorSearch/modes/labels.tsx @@ -8,9 +8,11 @@ export const getProductVariantLabel = (variant: Variant) => { return ( <> {variant.product.name} / {variant.name} - - ({variant.sku}) - + {variant.sku && ( + + ({variant.sku}) + + )} ); }; From 9e675c9740dd0650666dc129baf10e702f7c8edc Mon Sep 17 00:00:00 2001 From: Wojciech Date: Fri, 21 Mar 2025 11:39:14 +0100 Subject: [PATCH 09/15] handle label as react node --- src/components/NavigatorSearch/NavigatorSearch.tsx | 4 +++- .../NavigatorSearch/NavigatorSearchSection.tsx | 4 +++- src/components/NavigatorSearch/modes/catalog.ts | 2 +- src/misc.ts | 9 +++++++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/NavigatorSearch/NavigatorSearch.tsx b/src/components/NavigatorSearch/NavigatorSearch.tsx index f01d487b5e0..fefcbc3e520 100644 --- a/src/components/NavigatorSearch/NavigatorSearch.tsx +++ b/src/components/NavigatorSearch/NavigatorSearch.tsx @@ -143,7 +143,9 @@ const NavigatorSearch: React.FC = () => { (item ? item.label : "")} + itemToString={(item: QuickSearchAction) => + item && typeof item.label === "string" ? item.label : "" + } onSelect={(item: QuickSearchAction) => { const shouldRemainVisible = item?.onClick(); diff --git a/src/components/NavigatorSearch/NavigatorSearchSection.tsx b/src/components/NavigatorSearch/NavigatorSearchSection.tsx index d7c2e0dc151..033b7331732 100644 --- a/src/components/NavigatorSearch/NavigatorSearchSection.tsx +++ b/src/components/NavigatorSearch/NavigatorSearchSection.tsx @@ -47,7 +47,9 @@ const NavigatorSearchSection: React.FC = props => { active: "accent1Pressed", }} selected={highlightedIndex === index} - key={[item.label, item.type].join(":")} + key={[typeof item.label === "string" ? item.label : item.searchValue, item.type].join( + ":", + )} cursor="pointer" display="flex" flexDirection="row" diff --git a/src/components/NavigatorSearch/modes/catalog.ts b/src/components/NavigatorSearch/modes/catalog.ts index 992922bb5ae..70f3d85ff4b 100644 --- a/src/components/NavigatorSearch/modes/catalog.ts +++ b/src/components/NavigatorSearch/modes/catalog.ts @@ -83,7 +83,7 @@ export function searchInCatalog( })); const searchableItems = [...categories, ...collections, ...products, ...variants]; - const searchResults = fuzzySearch(searchableItems, search, ["searchValue"]); + const searchResults = fuzzySearch(searchableItems, search, ["searchValue"], 0.8); return searchResults; } diff --git a/src/misc.ts b/src/misc.ts index ff4c8661a3c..bc12efde7c6 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -625,7 +625,12 @@ const getAllRemovedRowsBeforeRowIndex = (rowIndex: number, removedRowsIndexs: nu export const getDatagridRowDataIndex = (rowIndex: number, removedRowsIndexs: number[]) => rowIndex + getAllRemovedRowsBeforeRowIndex(rowIndex, removedRowsIndexs).length; -export const fuzzySearch = (array: T[], query: string | undefined, keys: string[]) => { +export const fuzzySearch = ( + array: T[], + query: string | undefined, + keys: string[], + threshold = 0.3, +) => { if (!query) { return array; } @@ -633,7 +638,7 @@ export const fuzzySearch = (array: T[], query: string | undefined, keys: stri const fuse = new Fuse(array, { keys, includeScore: true, - threshold: 0.3, + threshold, }); return fuse.search(query.toLocaleLowerCase()).map(({ item }) => item); From 278f5c5c0f2d8fbfac9def310dcfb9794c8ccde2 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Fri, 21 Mar 2025 11:49:14 +0100 Subject: [PATCH 10/15] fix type issues --- src/components/NavigatorSearch/modes/commands/actions.ts | 1 + src/components/NavigatorSearch/modes/customers.ts | 4 ++++ src/components/NavigatorSearch/modes/default/views.ts | 1 + src/components/NavigatorSearch/modes/help.ts | 7 +++++++ src/components/NavigatorSearch/modes/orders.ts | 1 + 5 files changed, 14 insertions(+) diff --git a/src/components/NavigatorSearch/modes/commands/actions.ts b/src/components/NavigatorSearch/modes/commands/actions.ts index e302278add2..79634a92ecc 100644 --- a/src/components/NavigatorSearch/modes/commands/actions.ts +++ b/src/components/NavigatorSearch/modes/commands/actions.ts @@ -95,6 +95,7 @@ export function searchInCommands( return fuzzySearch(actions, search, ["label"]).map(action => ({ label: action.label, + searchValue: action.label, onClick: action.onClick, text: action.label, type: "action", diff --git a/src/components/NavigatorSearch/modes/customers.ts b/src/components/NavigatorSearch/modes/customers.ts index fb65d19228e..aa0dd6f725a 100644 --- a/src/components/NavigatorSearch/modes/customers.ts +++ b/src/components/NavigatorSearch/modes/customers.ts @@ -22,6 +22,10 @@ export function searchInCustomers( lastName: customer.lastName, }) : customer.email, + searchValue: + customer.firstName && customer.lastName + ? `${customer.firstName} ${customer.lastName}` + : customer.email, onClick: () => { navigate(customerUrl(customer.id)); diff --git a/src/components/NavigatorSearch/modes/default/views.ts b/src/components/NavigatorSearch/modes/default/views.ts index e9775815e03..0863996d46e 100644 --- a/src/components/NavigatorSearch/modes/default/views.ts +++ b/src/components/NavigatorSearch/modes/default/views.ts @@ -126,6 +126,7 @@ function searchInViews( return fuzzySearch(views, search, ["label"]).map(view => ({ label: view.label, + searchValue: view.label, onClick: () => { navigate(view.url); diff --git a/src/components/NavigatorSearch/modes/help.ts b/src/components/NavigatorSearch/modes/help.ts index aa388dc882f..c4adbf07e3c 100644 --- a/src/components/NavigatorSearch/modes/help.ts +++ b/src/components/NavigatorSearch/modes/help.ts @@ -12,6 +12,7 @@ function getHelpModeActions( return [ { label: intl.formatMessage(messages.noResults), + searchValue: query, onClick: () => true, type: "action", }, @@ -21,6 +22,7 @@ function getHelpModeActions( return [ { label: intl.formatMessage(messages.helpDefaultMode), + searchValue: "", onClick: () => { setMode("default"); @@ -31,6 +33,7 @@ function getHelpModeActions( }, { label: intl.formatMessage(messages.helpCommandsMode), + searchValue: "", onClick: () => { setMode("commands"); @@ -41,6 +44,7 @@ function getHelpModeActions( }, { label: intl.formatMessage(messages.helpOrdersMode), + searchValue: "", onClick: () => { setMode("orders"); @@ -51,6 +55,7 @@ function getHelpModeActions( }, { label: intl.formatMessage(messages.helpCustomersMode), + searchValue: "", onClick: () => { setMode("customers"); @@ -61,6 +66,7 @@ function getHelpModeActions( }, { label: intl.formatMessage(messages.helpCatalogMode), + searchValue: "", onClick: () => { setMode("catalog"); @@ -71,6 +77,7 @@ function getHelpModeActions( }, { label: intl.formatMessage(messages.helpMode), + searchValue: "", onClick: () => { setMode("help"); diff --git a/src/components/NavigatorSearch/modes/orders.ts b/src/components/NavigatorSearch/modes/orders.ts index 31e83b8e7d3..039b024037f 100644 --- a/src/components/NavigatorSearch/modes/orders.ts +++ b/src/components/NavigatorSearch/modes/orders.ts @@ -33,6 +33,7 @@ function getOrdersModeActions( label: intl.formatMessage(messages.goToOrder, { orderNumber, }), + searchValue: orderNumber, onClick: () => { navigate(orderUrl(order.id)); From 14aa73304e7cb53fe49b731b4d588bc7a3144a8f Mon Sep 17 00:00:00 2001 From: Wojciech Date: Fri, 21 Mar 2025 12:03:02 +0100 Subject: [PATCH 11/15] i18n --- locale/defaultMessages.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 8102dd89d70..08e1e5215c4 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -2835,9 +2835,6 @@ "context": "value input label", "string": "Discount value" }, - "GC8T3V": { - "string": "Product variant" - }, "GCPzKf": { "context": "ProductTypeDeleteWarningDialog single assigned items button label", "string": "View products" From 8b3017b0ffe47b12080740eb3f17d42807df8ebe Mon Sep 17 00:00:00 2001 From: Wojciech Date: Fri, 21 Mar 2025 14:37:39 +0100 Subject: [PATCH 12/15] Add missing commments --- src/components/NavigatorSearch/modes/catalog.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/NavigatorSearch/modes/catalog.test.ts b/src/components/NavigatorSearch/modes/catalog.test.ts index b65d7607b4f..d0bf2c620e6 100644 --- a/src/components/NavigatorSearch/modes/catalog.test.ts +++ b/src/components/NavigatorSearch/modes/catalog.test.ts @@ -10,6 +10,7 @@ describe("searchInCatalog", () => { const navigate = jest.fn(); it("should return variants if search is a sku", () => { + // Arrange const mockCatalog: SearchCatalogQuery = { productVariants: { edges: [ @@ -32,13 +33,16 @@ describe("searchInCatalog", () => { }, } as SearchCatalogQuery; + // Act const results = searchInCatalog("PROD-SMALL", intl, navigate, mockCatalog); + // Assert expect(results).toHaveLength(1); expect(results[0].searchValue).toContain("PROD-SMALL"); }); it("should return both product and its variant", () => { + // Arrange const mockCatalog: SearchCatalogQuery = { products: { edges: [ @@ -75,8 +79,10 @@ describe("searchInCatalog", () => { }, } as SearchCatalogQuery; + // Act const results = searchInCatalog("T-Shirt", intl, navigate, mockCatalog); + // Assert expect(results).toHaveLength(2); expect(results[0].label).toBe("T-Shirt"); expect(results[1].searchValue).toContain("PROD-SMALL"); From bfa8f1e4d20a93094c952fdccf81d6cfbb97bbcb Mon Sep 17 00:00:00 2001 From: Wojciech Date: Fri, 21 Mar 2025 15:16:46 +0100 Subject: [PATCH 13/15] restore original cmd --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 718fee7b269..5e951151463 100644 --- a/package.json +++ b/package.json @@ -266,7 +266,7 @@ "prestart": "npm run build-types", "test": "jest src/", "test:ci": "jest src/ --coverage", - "test:watch": "jest --watch src", + "test:watch": "jest --watch src/", "lint": "eslint \"{src,playwright}/**/*.@(tsx|ts|jsx|js)\" --fix", "lint:check-progress": "eslint-nibble \"src/**/*.@(tsx|ts|jsx|js)\"", "prebuild": "npm run build-types", From 9b0d673e704a70a584d4931c5455df1515316b00 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Mon, 24 Mar 2025 10:12:27 +0100 Subject: [PATCH 14/15] add description to messages --- locale/defaultMessages.json | 11 ++++++++--- src/components/NavigatorSearch/modes/messages.ts | 6 ++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index d79754e634a..40bbfcb54cc 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -603,6 +603,10 @@ "context": "expiry date section header", "string": "Expiry date" }, + "1JSu4L": { + "context": "Navigation search no parent category message", + "string": "This is root category" + }, "1LBYpE": { "context": "dialog header", "string": "Delete Menus" @@ -1401,9 +1405,6 @@ "context": "tooltip, submit form", "string": "Transaction is non-refundable. You have to send refund manually." }, - "6iaXL/": { - "string": "This is root category" - }, "6iw4VR": { "context": "delete variant dialog title", "string": "Delete Product Variants" @@ -3921,6 +3922,10 @@ "context": "customer details, header", "string": "{fullName} Details" }, + "MtkXb6": { + "context": "Navigation search variant item type name", + "string": "Variant" + }, "MwfSVA": { "context": "input helper text", "string": "Customers can not add quantities to a single cart above the limit when value is provided." diff --git a/src/components/NavigatorSearch/modes/messages.ts b/src/components/NavigatorSearch/modes/messages.ts index 0e9040a0a06..d499149a12b 100644 --- a/src/components/NavigatorSearch/modes/messages.ts +++ b/src/components/NavigatorSearch/modes/messages.ts @@ -103,11 +103,13 @@ const messages = defineMessages({ }, root: { defaultMessage: "This is root category", - id: "6iaXL/", + description: "Navigation search no parent category message", + id: "1JSu4L", }, variant: { defaultMessage: "Variant", - id: "OK5+Fh", + description: "Navigation search variant item type name", + id: "MtkXb6", }, }); From bcf8c71e3b325947c7d285069e454008d361c817 Mon Sep 17 00:00:00 2001 From: Wojciech Date: Tue, 25 Mar 2025 11:48:10 +0100 Subject: [PATCH 15/15] cr fixes --- src/components/NavigatorSearch/NavigatorSearch.tsx | 2 ++ src/components/NavigatorSearch/NavigatorThumbnail.tsx | 2 +- src/components/NavigatorSearch/modes/catalog.test.ts | 8 +++----- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/NavigatorSearch/NavigatorSearch.tsx b/src/components/NavigatorSearch/NavigatorSearch.tsx index fefcbc3e520..ada883ccb5d 100644 --- a/src/components/NavigatorSearch/NavigatorSearch.tsx +++ b/src/components/NavigatorSearch/NavigatorSearch.tsx @@ -144,6 +144,8 @@ const NavigatorSearch: React.FC = () => { + // 'label' can be string for non-search results (actions) + // and ReactNode for search results item && typeof item.label === "string" ? item.label : "" } onSelect={(item: QuickSearchAction) => { diff --git a/src/components/NavigatorSearch/NavigatorThumbnail.tsx b/src/components/NavigatorSearch/NavigatorThumbnail.tsx index 4e2e1a11080..28859f5b835 100644 --- a/src/components/NavigatorSearch/NavigatorThumbnail.tsx +++ b/src/components/NavigatorSearch/NavigatorThumbnail.tsx @@ -36,7 +36,7 @@ export const NavigatorThumbnail = ({ display="flex" alignItems="center" justifyContent="center" - __padding="2px" + padding={0.5} borderRadius={4} borderStyle="solid" borderWidth={1} diff --git a/src/components/NavigatorSearch/modes/catalog.test.ts b/src/components/NavigatorSearch/modes/catalog.test.ts index d0bf2c620e6..4e12f05032d 100644 --- a/src/components/NavigatorSearch/modes/catalog.test.ts +++ b/src/components/NavigatorSearch/modes/catalog.test.ts @@ -1,12 +1,10 @@ import { SearchCatalogQuery } from "@dashboard/graphql"; -import { IntlShape } from "react-intl"; +import { intlMock } from "@test/intl"; import { searchInCatalog } from "./catalog"; -describe("searchInCatalog", () => { - const intl = { - formatMessage: ({ defaultMessage }) => defaultMessage, - } as IntlShape; +describe("NavigatorSearch / modes / searchInCatalog", () => { + const intl = intlMock; const navigate = jest.fn(); it("should return variants if search is a sku", () => {