Skip to content

Commit

Permalink
Fix product variant crash when many warehouses by load on scroll (#4932)
Browse files Browse the repository at this point in the history
* Fetch warehouses async

* Update create and update page

* Improve loader and button

* useInfityScroll hook

* Center loader

* Improve edge cases in infinity scroll

* Improve useWarehouseSearch variables typing

* Keep warehouses fetch in view component

* Fix infinity loading

* Add changesset

* Fix typos

* Remove not usedd hook useAllWarehouses

* Use event listener insted of expose onScroll funtion

* use scroll  instead

* Back to scroll again Yeah!

* Fix loading more on init
  • Loading branch information
poulch authored and Cloud11PL committed Jun 19, 2024
1 parent fe50d2f commit 209b999
Show file tree
Hide file tree
Showing 22 changed files with 350 additions and 202 deletions.
5 changes: 5 additions & 0 deletions .changeset/swift-balloons-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"saleor-dashboard": patch
---

You can now assign warehouses in product variant page without page crash
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SingleAutocompleteSelectField from "@dashboard/components/SingleAutocompleteSelectField";
import CardAddItemsFooter from "@dashboard/products/components/ProductStocks/CardAddItemsFooter";
import CardAddItemsFooter from "@dashboard/products/components/ProductStocks/components/CardAddItemsFooter";
import { mapNodeToChoice } from "@dashboard/utils/maps";
import { ClickAwayListener } from "@material-ui/core";
import React, { useEffect, useRef, useState } from "react";
Expand Down
5 changes: 3 additions & 2 deletions src/graphql/hooks.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16471,12 +16471,12 @@ export type SearchVariantsWithProductDataQueryHookResult = ReturnType<typeof use
export type SearchVariantsWithProductDataLazyQueryHookResult = ReturnType<typeof useSearchVariantsWithProductDataLazyQuery>;
export type SearchVariantsWithProductDataQueryResult = Apollo.QueryResult<Types.SearchVariantsWithProductDataQuery, Types.SearchVariantsWithProductDataQueryVariables>;
export const SearchWarehousesDocument = gql`
query SearchWarehouses($after: String, $first: Int!, $query: String!) {
query SearchWarehouses($after: String, $first: Int!, $query: String!, $channnelsId: [ID!]) {
search: warehouses(
after: $after
first: $first
sortBy: {direction: ASC, field: NAME}
filter: {search: $query}
filter: {search: $query, channels: $channnelsId}
) {
totalCount
edges {
Expand Down Expand Up @@ -16507,6 +16507,7 @@ export const SearchWarehousesDocument = gql`
* after: // value for 'after'
* first: // value for 'first'
* query: // value for 'query'
* channnelsId: // value for 'channnelsId'
* },
* });
*/
Expand Down
1 change: 1 addition & 0 deletions src/graphql/types.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11885,6 +11885,7 @@ export type SearchWarehousesQueryVariables = Exact<{
after?: InputMaybe<Scalars['String']>;
first: Scalars['Int'];
query: Scalars['String'];
channnelsId?: InputMaybe<Array<Scalars['ID']> | Scalars['ID']>;
}>;


Expand Down
77 changes: 77 additions & 0 deletions src/hooks/useInfinityScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useCallback, useEffect, useRef } from "react";

import useDebounce from "./useDebounce";

const SCROLL_THRESHOLD = 100;
const DEBOUNCE_TIME = 500;

export const useInfinityScroll = <TElementRef extends HTMLElement>({
onLoadMore,
threshold = SCROLL_THRESHOLD,
debounceTime = DEBOUNCE_TIME,
}: {
onLoadMore: () => void;
threshold?: number;
debounceTime?: number;
}) => {
const scrollRef = useRef<TElementRef>(null);

const shouldLoadMore = useCallback(() => {
if (!scrollRef.current) {
return false;
}

const totalScrollHeight = scrollRef.current.scrollHeight;
const scrollTop = scrollRef.current.scrollTop;
const clientHeight = scrollRef.current.clientHeight;

if (scrollTop === 0 && totalScrollHeight === 0 && clientHeight === 0) {
return false;
}

const scrolledHeight = scrollTop + clientHeight;
const scrollBottom = totalScrollHeight - scrolledHeight;

return scrollBottom < threshold;
}, [threshold]);

const handleInfiniteScroll = () => {
if (!scrollRef.current) {
return;
}

if (shouldLoadMore()) {
onLoadMore();
}
};

const debouncedHandleInfiniteScroll = useDebounce(
handleInfiniteScroll,
debounceTime,
);

useEffect(() => {
if (!scrollRef.current) {
return;
}

const callback = () => debouncedHandleInfiniteScroll();

scrollRef.current.addEventListener("scroll", callback);

return () => {
scrollRef.current?.removeEventListener("scroll", callback);
};
}, [debouncedHandleInfiniteScroll]);

useEffect(() => {
// On init check thresholdd and load more if needed
if (shouldLoadMore()) {
onLoadMore();
}
}, [onLoadMore, shouldLoadMore]);

return {
scrollRef,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const OrderChangeWarehouseDialog: React.FC<
} = useWarehouseSearch({
variables: {
after: null,
channnelsId: null,
first: 20,
query: "",
},
Expand Down
19 changes: 15 additions & 4 deletions src/products/components/ProductCreatePage/ProductCreatePage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @ts-strict-ignore
import { QueryResult } from "@apollo/client";
import {
getReferenceAttributeEntityTypeFromAttribute,
mergeAttributeValues,
Expand Down Expand Up @@ -38,6 +39,7 @@ import {
productListUrl,
} from "@dashboard/products/urls";
import { getChoices } from "@dashboard/products/utils/data";
import { mapEdgesToItems } from "@dashboard/utils/maps";
import { Box, Option } from "@saleor/macaw-ui-next";
import React from "react";
import { useIntl } from "react-intl";
Expand Down Expand Up @@ -75,7 +77,6 @@ interface ProductCreatePageProps {
header: string;
saveButtonBarState: ConfirmButtonTransitionState;
weightUnit: string;
warehouses: RelayToFlat<SearchWarehousesQuery["search"]>;
taxClasses: TaxClassBaseFragment[];
fetchMoreTaxClasses: FetchMoreProps;
selectedProductType?: ProductTypeQuery["productType"];
Expand All @@ -96,6 +97,8 @@ interface ProductCreatePageProps {
onCloseDialog: (currentParams?: ProductCreateUrlQueryParams) => void;
onSelectProductType: (productTypeId: string) => void;
onSubmit?: (data: ProductCreateData) => any;
fetchMoreWarehouses: () => void;
searchWarehousesResult: QueryResult<SearchWarehousesQuery>;
}

export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
Expand All @@ -118,7 +121,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
referencePages = [],
referenceProducts = [],
saveButtonBarState,
warehouses,
taxClasses,
fetchMoreTaxClasses,
selectedProductType,
Expand All @@ -139,6 +141,8 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
onCloseDialog,
onSelectProductType,
onAttributeSelectBlur,
fetchMoreWarehouses,
searchWarehousesResult,
}: ProductCreatePageProps) => {
const intl = useIntl();
const navigate = useNavigator();
Expand Down Expand Up @@ -209,7 +213,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
setSelectedTaxClass={setSelectedTaxClass}
setChannels={onChannelsChange}
taxClasses={taxClassChoices}
warehouses={warehouses}
currentChannels={currentChannels}
fetchReferencePages={fetchReferencePages}
fetchMoreReferencePages={fetchMoreReferencePages}
Expand Down Expand Up @@ -282,12 +285,20 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
/>
<ProductStocks
data={data}
warehouses={
mapEdgesToItems(searchWarehousesResult?.data?.search) ??
[]
}
fetchMoreWarehouses={fetchMoreWarehouses}
hasMoreWarehouses={
searchWarehousesResult?.data?.search?.pageInfo
?.hasNextPage
}
disabled={loading}
hasVariants={false}
onFormDataChange={change}
errors={errors}
stocks={data.stocks}
warehouses={warehouses}
onChange={handlers.changeStock}
onWarehouseStockAdd={handlers.addStock}
onWarehouseStockDelete={handlers.deleteStock}
Expand Down
9 changes: 4 additions & 5 deletions src/products/components/ProductCreatePage/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
SearchPagesQuery,
SearchProductsQuery,
SearchProductTypesQuery,
SearchWarehousesQuery,
} from "@dashboard/graphql";
import useForm, {
CommonUseFormResultWithHandlers,
Expand Down Expand Up @@ -130,7 +129,8 @@ export interface ProductCreateHandlers
Record<"selectAttributeReference", FormsetChange<string[]>>,
Record<"selectAttributeFile", FormsetChange<File>>,
Record<"reorderAttributeValue", FormsetChange<ReorderEvent>>,
Record<"addStock" | "deleteStock", (id: string) => void> {
Record<"addStock", (id: string, label: string) => void>,
Record<"deleteStock", (id: string) => void> {
changePreorderEndDate: FormChange;
fetchReferences: (value: string) => void;
fetchMoreReferences: FetchMoreProps;
Expand Down Expand Up @@ -167,7 +167,6 @@ export interface UseProductCreateFormOpts
setChannels: (channels: ChannelData[]) => void;
selectedCollections: MultiAutocompleteChoiceType[];
productTypes: RelayToFlat<SearchProductTypesQuery["search"]>;
warehouses: RelayToFlat<SearchWarehousesQuery["search"]>;
currentChannels: ChannelData[];
referencePages: RelayToFlat<SearchPagesQuery["search"]>;
referenceProducts: RelayToFlat<SearchProductsQuery["search"]>;
Expand Down Expand Up @@ -321,14 +320,14 @@ function useProductCreateForm(
triggerChange();
stocks.change(id, value);
};
const handleStockAdd = (id: string) => {
const handleStockAdd = (id: string, label: string) => {
triggerChange();
stocks.add({
data: {
quantityAllocated: 0,
},
id,
label: opts.warehouses.find(warehouse => warehouse.id === id).name,
label,
value: "0",
});
};
Expand Down
76 changes: 25 additions & 51 deletions src/products/components/ProductStocks/ProductStocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ import {
Box,
Button,
Checkbox,
Dropdown,
Input,
List,
Text,
TrashBinIcon,
vars,
} from "@saleor/macaw-ui-next";
import React from "react";
import React, { useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl";

import { ProductStocksAssignWarehouses } from "./components/ProductStocksAssignWarehouses";
import { messages } from "./messages";

export interface ProductStockFormsetData {
quantityAllocated: number;
}
export type ProductStockInput = FormsetAtomicData<
ProductStockFormsetData,
string,
string
>;
export interface ProductStockFormData {
Expand All @@ -52,9 +52,11 @@ export interface ProductStocksProps {
warehouses: WarehouseFragment[];
onChange: FormsetChange;
onFormDataChange: FormChange;
onWarehouseStockAdd: (warehouseId: string) => void;
onWarehouseStockAdd: (warehouseId: string, warehouseName: string) => void;
onWarehouseStockDelete: (warehouseId: string) => void;
onWarehouseConfigure: () => void;
fetchMoreWarehouses: () => void;
hasMoreWarehouses: boolean;
}

export const ProductStocks: React.FC<ProductStocksProps> = ({
Expand All @@ -63,25 +65,30 @@ export const ProductStocks: React.FC<ProductStocksProps> = ({
hasVariants,
errors,
stocks,
warehouses,
productVariantChannelListings = [],
warehouses,
hasMoreWarehouses,
onChange,
onFormDataChange,
onWarehouseStockAdd,
onWarehouseStockDelete,
onWarehouseConfigure,
fetchMoreWarehouses,
}) => {
const intl = useIntl();
const [lastStockRowFocus, setLastStockRowFocus] = React.useState(false);
const formErrors = getFormErrors(["sku"], errors);

const stocksIds = useMemo(() => stocks.map(stock => stock.id), [stocks]);

const warehousesToAssign =
warehouses?.filter(
warehouse => !stocks.some(stock => stock.id === warehouse.id),
) || [];
const formErrors = getFormErrors(["sku"], errors);
warehouses?.filter(warehouse => !stocksIds.includes(warehouse.id)) || [];

const handleWarehouseStockAdd = (warehouseId: string) => {
onWarehouseStockAdd(warehouseId);
const handleWarehouseStockAdd = (
warehouseId: string,
warehouseName: string,
) => {
onWarehouseStockAdd(warehouseId, warehouseName);
setLastStockRowFocus(true);
};

Expand Down Expand Up @@ -246,46 +253,13 @@ export const ProductStocks: React.FC<ProductStocksProps> = ({

{productVariantChannelListings?.length > 0 &&
warehouses?.length > 0 &&
warehousesToAssign.length > 0 && (
<Dropdown>
<Dropdown.Trigger>
<Button
type="button"
variant="secondary"
marginTop={5}
data-test-id="assign-warehouse-button"
>
<FormattedMessage {...messages.assignWarehouse} />
</Button>
</Dropdown.Trigger>

<Dropdown.Content align="end">
<Box>
<List
id="warehouse-list"
padding={2}
borderRadius={4}
boxShadow="defaultOverlay"
backgroundColor="default1"
__maxHeight={400}
overflowY="auto"
>
{warehousesToAssign.map(warehouse => (
<Dropdown.Item key={warehouse.id}>
<List.Item
paddingX={1.5}
paddingY={2}
borderRadius={4}
onClick={() => handleWarehouseStockAdd(warehouse.id)}
>
<Text>{warehouse.name}</Text>
</List.Item>
</Dropdown.Item>
))}
</List>
</Box>
</Dropdown.Content>
</Dropdown>
(warehousesToAssign.length > 0 || hasMoreWarehouses) && (
<ProductStocksAssignWarehouses
warehousesToAssign={warehousesToAssign}
hasMoreWarehouses={hasMoreWarehouses}
loadMoreWarehouses={fetchMoreWarehouses}
onWarehouseSelect={handleWarehouseStockAdd}
/>
)}
</DashboardCard.Content>
</DashboardCard>
Expand Down
Loading

0 comments on commit 209b999

Please sign in to comment.