diff --git a/CHANGELOG.md b/CHANGELOG.md index f418f35b4..82e2bea2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ All notable, unreleased changes to this project will be documented in this file. - Add manage product types and attributes permission - #768 by @orzechdev - Fix isPublished and isAvailable behaviour for products, collections and pages - #780 by @mmarkusik - Add metadata editor to page views - #782 by @dominik-zeglen +- Add missing infinite scroll to searches - #793 by @dominik-zeglen ## 2.10.1 diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index bb9d0b772..414af4209 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -1400,23 +1400,15 @@ "context": "section header", "string": "App Status" }, - "src_dot_components_dot_AssignCategoryDialog_dot_1305061437": { - "string": "Search Categories" - }, - "src_dot_components_dot_AssignCategoryDialog_dot_190977792": { + "src_dot_components_dot_AssignCategoryDialog_dot_3125506097": { "context": "dialog header", - "string": "Assign Categories" + "string": "Assign Category" }, "src_dot_components_dot_AssignCategoryDialog_dot_3690273268": { "string": "Search by category name, etc..." }, - "src_dot_components_dot_AssignCategoryDialog_dot_3973677075": { - "context": "button", - "string": "Assign categories" - }, - "src_dot_components_dot_AssignCollectionDialog_dot_1035511604": { - "context": "button", - "string": "Assign collections" + "src_dot_components_dot_AssignCategoryDialog_dot_3841025483": { + "string": "Search Category" }, "src_dot_components_dot_AssignCollectionDialog_dot_2605414502": { "string": "Search by collection name, etc..." @@ -1428,6 +1420,10 @@ "src_dot_components_dot_AssignCollectionDialog_dot_4057224233": { "string": "Search Collection" }, + "src_dot_components_dot_AssignContainerDialog_dot_1731102929": { + "context": "button", + "string": "Assign" + }, "src_dot_components_dot_AssignProductDialog_dot_2100305525": { "context": "button", "string": "Assign products" diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index 9ca6d0798..e1e5a0af5 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -60,7 +60,7 @@ export const CollectionDetails: React.FC = ({ ); const paginate = usePaginator(); const intl = useIntl(); - const { search, result } = useProductSearch({ + const { search, loadMore, result } = useProductSearch({ variables: DEFAULT_INITIAL_SEARCH_DATA }); const [updateMetadata] = useMetadataUpdate({}); @@ -296,8 +296,10 @@ export const CollectionDetails: React.FC = ({ /> diff --git a/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx b/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx index 7b3bc1ed5..f0077a198 100644 --- a/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx +++ b/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx @@ -1,185 +1,41 @@ -import Button from "@material-ui/core/Button"; -import CircularProgress from "@material-ui/core/CircularProgress"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import { makeStyles } from "@material-ui/core/styles"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableRow from "@material-ui/core/TableRow"; -import TextField from "@material-ui/core/TextField"; -import ConfirmButton, { - ConfirmButtonTransitionState -} from "@saleor/components/ConfirmButton"; -import FormSpacer from "@saleor/components/FormSpacer"; -import ResponsiveTable from "@saleor/components/ResponsiveTable"; -import useSearchQuery from "@saleor/hooks/useSearchQuery"; -import { buttonMessages } from "@saleor/intl"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; -import Checkbox from "../Checkbox"; +import AssignContainerDialog, { + AssignContainerDialogProps +} from "../AssignContainerDialog"; -export interface FormData { +interface AssignCategoryDialogProps + extends Omit { categories: SearchCategories_search_edges_node[]; - query: string; } -const useStyles = makeStyles( - { - avatar: { - "&:first-child": { - paddingLeft: 0 - } - }, - checkboxCell: { - paddingLeft: 0 - }, - overflow: { - overflowY: "visible" - }, - wideCell: { - width: "100%" - } - }, - { name: "AssignCategoryDialog" } -); - -interface AssignCategoriesDialogProps { - categories: SearchCategories_search_edges_node[]; - confirmButtonState: ConfirmButtonTransitionState; - open: boolean; - loading: boolean; - onClose: () => void; - onFetch: (value: string) => void; - onSubmit: (data: SearchCategories_search_edges_node[]) => void; -} - -function handleCategoryAssign( - product: SearchCategories_search_edges_node, - isSelected: boolean, - selectedCategories: SearchCategories_search_edges_node[], - setSelectedCategories: (data: SearchCategories_search_edges_node[]) => void -) { - if (isSelected) { - setSelectedCategories( - selectedCategories.filter( - selectedProduct => selectedProduct.id !== product.id - ) - ); - } else { - setSelectedCategories([...selectedCategories, product]); - } -} - -const AssignCategoriesDialog: React.FC = props => { - const { - confirmButtonState, - open, - loading, - categories: categories, - onClose, - onFetch, - onSubmit - } = props; - const classes = useStyles(props); - +const AssignCategoryDialog: React.FC = ({ + categories, + ...rest +}) => { const intl = useIntl(); - const [query, onQueryChange] = useSearchQuery(onFetch); - const [selectedCategories, setSelectedCategories] = React.useState< - SearchCategories_search_edges_node[] - >([]); - - const handleSubmit = () => onSubmit(selectedCategories); return ( - - - - - - - }} - /> - - - - {categories && - categories.map(category => { - const isSelected = !!selectedCategories.find( - selectedCategories => selectedCategories.id === category.id - ); - - return ( - - - - handleCategoryAssign( - category, - isSelected, - selectedCategories, - setSelectedCategories - ) - } - /> - - - {category.name} - - - ); - })} - - - - - - - - - - + ); }; -AssignCategoriesDialog.displayName = "AssignCategoriesDialog"; -export default AssignCategoriesDialog; + +AssignCategoryDialog.displayName = "AssignCategoryDialog"; +export default AssignCategoryDialog; diff --git a/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx b/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx index bf46d3de3..0ee4d7146 100644 --- a/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx +++ b/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx @@ -1,185 +1,41 @@ -import Button from "@material-ui/core/Button"; -import CircularProgress from "@material-ui/core/CircularProgress"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import { makeStyles } from "@material-ui/core/styles"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableRow from "@material-ui/core/TableRow"; -import TextField from "@material-ui/core/TextField"; -import ResponsiveTable from "@saleor/components/ResponsiveTable"; -import useSearchQuery from "@saleor/hooks/useSearchQuery"; -import { buttonMessages } from "@saleor/intl"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; -import Checkbox from "../Checkbox"; -import ConfirmButton, { - ConfirmButtonTransitionState -} from "../ConfirmButton/ConfirmButton"; -import FormSpacer from "../FormSpacer"; +import AssignContainerDialog, { + AssignContainerDialogProps +} from "../AssignContainerDialog"; -export interface FormData { +interface AssignCollectionDialogProps + extends Omit { collections: SearchCollections_search_edges_node[]; - query: string; } -const useStyles = makeStyles( - { - avatar: { - "&:first-child": { - paddingLeft: 0 - } - }, - checkboxCell: { - paddingLeft: 0 - }, - overflow: { - overflowY: "visible" - }, - wideCell: { - width: "100%" - } - }, - { name: "AssignCollectionDialog" } -); - -interface AssignCollectionDialogProps { - collections: SearchCollections_search_edges_node[]; - confirmButtonState: ConfirmButtonTransitionState; - open: boolean; - loading: boolean; - onClose: () => void; - onFetch: (value: string) => void; - onSubmit: (data: SearchCollections_search_edges_node[]) => void; -} - -function handleCollectionAssign( - product: SearchCollections_search_edges_node, - isSelected: boolean, - selectedCollections: SearchCollections_search_edges_node[], - setSelectedCollections: (data: SearchCollections_search_edges_node[]) => void -) { - if (isSelected) { - setSelectedCollections( - selectedCollections.filter( - selectedProduct => selectedProduct.id !== product.id - ) - ); - } else { - setSelectedCollections([...selectedCollections, product]); - } -} - -const AssignCollectionDialog: React.FC = props => { - const { - confirmButtonState, - open, - loading, - collections, - onClose, - onFetch, - onSubmit - } = props; - const classes = useStyles(props); - +const AssignCollectionDialog: React.FC = ({ + collections, + ...rest +}) => { const intl = useIntl(); - const [query, onQueryChange] = useSearchQuery(onFetch); - const [selectedCollections, setSelectedCollections] = React.useState< - SearchCollections_search_edges_node[] - >([]); - - const handleSubmit = () => onSubmit(selectedCollections); return ( - - - - - - - }} - /> - - - - {collections && - collections.map(collection => { - const isSelected = !!selectedCollections.find( - selectedCollection => selectedCollection.id === collection.id - ); - - return ( - - - - handleCollectionAssign( - collection, - isSelected, - selectedCollections, - setSelectedCollections - ) - } - /> - - - {collection.name} - - - ); - })} - - - - - - - - - - + ); }; + AssignCollectionDialog.displayName = "AssignCollectionDialog"; export default AssignCollectionDialog; diff --git a/src/components/AssignContainerDialog/AssignContainerDialog.tsx b/src/components/AssignContainerDialog/AssignContainerDialog.tsx new file mode 100644 index 000000000..23b3e29b4 --- /dev/null +++ b/src/components/AssignContainerDialog/AssignContainerDialog.tsx @@ -0,0 +1,204 @@ +import Button from "@material-ui/core/Button"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Dialog from "@material-ui/core/Dialog"; +import DialogActions from "@material-ui/core/DialogActions"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import { makeStyles } from "@material-ui/core/styles"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableRow from "@material-ui/core/TableRow"; +import TextField from "@material-ui/core/TextField"; +import ResponsiveTable from "@saleor/components/ResponsiveTable"; +import useSearchQuery from "@saleor/hooks/useSearchQuery"; +import { buttonMessages } from "@saleor/intl"; +import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle"; +import { FetchMoreProps, Node } from "@saleor/types"; +import React from "react"; +import InfiniteScroll from "react-infinite-scroller"; +import { FormattedMessage } from "react-intl"; + +import Checkbox from "../Checkbox"; +import ConfirmButton, { + ConfirmButtonTransitionState +} from "../ConfirmButton/ConfirmButton"; +import FormSpacer from "../FormSpacer"; + +export interface FormData { + containers: string[]; + query: string; +} + +const useStyles = makeStyles( + { + avatar: { + "&:first-child": { + paddingLeft: 0 + } + }, + checkboxCell: { + paddingLeft: 0 + }, + wideCell: { + width: "100%" + } + }, + { name: "AssignContainerDialog" } +); + +interface Container extends Node { + name: string; +} +export interface AssignContainerDialogProps extends FetchMoreProps { + confirmButtonState: ConfirmButtonTransitionState; + containers: Container[]; + loading: boolean; + open: boolean; + search: Record<"label" | "placeholder", string>; + title: string; + onClose: () => void; + onFetch: (value: string) => void; + onSubmit: (data: string[]) => void; +} + +function handleContainerAssign( + containerId: string, + isSelected: boolean, + selectedContainers: string[], + setSelectedContainers: (data: string[]) => void +) { + if (isSelected) { + setSelectedContainers( + selectedContainers.filter( + selectedContainer => selectedContainer !== containerId + ) + ); + } else { + setSelectedContainers([...selectedContainers, containerId]); + } +} + +const AssignContainerDialog: React.FC = props => { + const { + confirmButtonState, + containers, + hasMore, + loading, + open, + search, + title, + onClose, + onFetch, + onFetchMore, + onSubmit + } = props; + const classes = useStyles(props); + const scrollableDialogClasses = useScrollableDialogStyle({}); + + const [query, onQueryChange] = useSearchQuery(onFetch); + const [selectedContainers, setSelectedContainers] = React.useState( + [] + ); + const container = React.useRef(); + + const handleSubmit = () => onSubmit(selectedContainers); + + const containerHeight = container.current?.scrollHeight - 130; + + return ( + + {title} + + + }} + /> + +
+ + +
+ } + threshold={10} + > + + + {containers?.map(container => { + const isSelected = !!selectedContainers.find( + selectedContainer => selectedContainer === container.id + ); + + return ( + + + + handleContainerAssign( + container.id, + isSelected, + selectedContainers, + setSelectedContainers + ) + } + /> + + + {container.name} + + + ); + })} + + + + +
+ + + + + + +
+ ); +}; +AssignContainerDialog.displayName = "AssignContainerDialog"; +export default AssignContainerDialog; diff --git a/src/components/AssignContainerDialog/index.ts b/src/components/AssignContainerDialog/index.ts new file mode 100644 index 000000000..523a573e8 --- /dev/null +++ b/src/components/AssignContainerDialog/index.ts @@ -0,0 +1,2 @@ +export * from "./AssignContainerDialog"; +export { default } from "./AssignContainerDialog"; diff --git a/src/components/AssignProductDialog/AssignProductDialog.tsx b/src/components/AssignProductDialog/AssignProductDialog.tsx index 92a2a9a0c..10f1431de 100644 --- a/src/components/AssignProductDialog/AssignProductDialog.tsx +++ b/src/components/AssignProductDialog/AssignProductDialog.tsx @@ -19,7 +19,10 @@ import useSearchQuery from "@saleor/hooks/useSearchQuery"; import { buttonMessages } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts"; +import useScrollableDialogStyle from "@saleor/styles/useScrollableDialogStyle"; +import { FetchMoreProps } from "@saleor/types"; import React from "react"; +import InfiniteScroll from "react-infinite-scroller"; import { FormattedMessage, useIntl } from "react-intl"; import Checkbox from "../Checkbox"; @@ -43,18 +46,12 @@ const useStyles = makeStyles( }, colName: { paddingLeft: 0 - }, - overflow: { - overflowY: "visible" - }, - scrollArea: { - overflowY: "scroll" } }, { name: "AssignProductDialog" } ); -export interface AssignProductDialogProps { +export interface AssignProductDialogProps extends FetchMoreProps { confirmButtonState: ConfirmButtonTransitionState; open: boolean; products: SearchProducts_search_edges_node[]; @@ -84,28 +81,34 @@ function handleProductAssign( const AssignProductDialog: React.FC = props => { const { confirmButtonState, + hasMore, open, loading, products, onClose, onFetch, + onFetchMore, onSubmit } = props; const classes = useStyles(props); + const scrollableDialogClasses = useScrollableDialogStyle({}); const intl = useIntl(); const [query, onQueryChange] = useSearchQuery(onFetch); const [selectedProducts, setSelectedProducts] = React.useState< SearchProducts_search_edges_node[] >([]); + const container = React.useRef(); const handleSubmit = () => onSubmit(selectedProducts); + const containerHeight = container.current?.scrollHeight - 130; + return ( @@ -115,7 +118,10 @@ const AssignProductDialog: React.FC = props => { description="dialog header" /> - + = props => { }} /> -
- - - {products && - products.map(product => { - const isSelected = selectedProducts.some( - selectedProduct => selectedProduct.id === product.id - ); +
+ + +
+ } + threshold={10} + > + + + {products && + products.map(product => { + const isSelected = selectedProducts.some( + selectedProduct => selectedProduct.id === product.id + ); - return ( - - product.thumbnail.url)} - /> - - {product.name} - - - - handleProductAssign( - product, - isSelected, - selectedProducts, - setSelectedProducts - ) - } + return ( + + product.thumbnail.url)} /> - - - ); - })} - - + + {product.name} + + + + handleProductAssign( + product, + isSelected, + selectedProducts, + setSelectedProducts + ) + } + /> + + + ); + })} +
+
+
diff --git a/src/discounts/views/SaleDetails.tsx b/src/discounts/views/SaleDetails.tsx index dbafd9c36..d14b77911 100644 --- a/src/discounts/views/SaleDetails.tsx +++ b/src/discounts/views/SaleDetails.tsx @@ -69,18 +69,21 @@ export const SaleDetails: React.FC = ({ id, params }) => { ); const intl = useIntl(); const { + loadMore: loadMoreCategories, search: searchCategories, result: searchCategoriesOpts } = useCategorySearch({ variables: DEFAULT_INITIAL_SEARCH_DATA }); const { + loadMore: loadMoreCollections, search: searchCollections, result: searchCollectionsOpts } = useCollectionSearch({ variables: DEFAULT_INITIAL_SEARCH_DATA }); const { + loadMore: loadMoreProducts, search: searchProducts, result: searchProductsOpts } = useProductSearch({ @@ -327,8 +330,13 @@ export const SaleDetails: React.FC = ({ id, params }) => { /> @@ -361,8 +369,13 @@ export const SaleDetails: React.FC = ({ id, params }) => { ) )} confirmButtonState={saleCataloguesAddOpts.status} + hasMore={ + searchCategoriesOpts.data?.search.pageInfo + .hasNextPage + } open={params.action === "assign-category"} onFetch={searchCategories} + onFetchMore={loadMoreCategories} loading={searchCategoriesOpts.loading} onClose={closeModal} onSubmit={categories => @@ -371,9 +384,7 @@ export const SaleDetails: React.FC = ({ id, params }) => { ...paginationState, id, input: { - categories: categories.map( - product => product.id - ) + categories } } }) @@ -388,8 +399,13 @@ export const SaleDetails: React.FC = ({ id, params }) => { ) )} confirmButtonState={saleCataloguesAddOpts.status} + hasMore={ + searchCollectionsOpts.data?.search.pageInfo + .hasNextPage + } open={params.action === "assign-collection"} onFetch={searchCollections} + onFetchMore={loadMoreCollections} loading={searchCollectionsOpts.loading} onClose={closeModal} onSubmit={collections => @@ -398,9 +414,7 @@ export const SaleDetails: React.FC = ({ id, params }) => { ...paginationState, id, input: { - collections: collections.map( - product => product.id - ) + collections } } }) diff --git a/src/discounts/views/VoucherDetails.tsx b/src/discounts/views/VoucherDetails.tsx index f59c90a34..d227497a2 100644 --- a/src/discounts/views/VoucherDetails.tsx +++ b/src/discounts/views/VoucherDetails.tsx @@ -71,18 +71,21 @@ export const VoucherDetails: React.FC = ({ ); const intl = useIntl(); const { + loadMore: loadMoreCategories, search: searchCategories, result: searchCategoriesOpts } = useCategorySearch({ variables: DEFAULT_INITIAL_SEARCH_DATA }); const { + loadMore: loadMoreCollections, search: searchCollections, result: searchCollectionsOpts } = useCollectionSearch({ variables: DEFAULT_INITIAL_SEARCH_DATA }); const { + loadMore: loadMoreProducts, search: searchProducts, result: searchProductsOpts } = useProductSearch({ @@ -407,8 +410,13 @@ export const VoucherDetails: React.FC = ({ confirmButtonState={ voucherCataloguesAddOpts.status } + hasMore={ + searchCategoriesOpts.data?.search.pageInfo + .hasNextPage + } open={params.action === "assign-category"} onFetch={searchCategories} + onFetchMore={loadMoreCategories} loading={searchCategoriesOpts.loading} onClose={closeModal} onSubmit={categories => @@ -417,9 +425,7 @@ export const VoucherDetails: React.FC = ({ ...paginationState, id, input: { - categories: categories.map( - product => product.id - ) + categories } } }) @@ -436,8 +442,13 @@ export const VoucherDetails: React.FC = ({ confirmButtonState={ voucherCataloguesAddOpts.status } + hasMore={ + searchCollectionsOpts.data?.search.pageInfo + .hasNextPage + } open={params.action === "assign-collection"} onFetch={searchCollections} + onFetchMore={loadMoreCollections} loading={searchCollectionsOpts.loading} onClose={closeModal} onSubmit={collections => @@ -446,9 +457,7 @@ export const VoucherDetails: React.FC = ({ ...paginationState, id, input: { - collections: collections.map( - product => product.id - ) + collections } } }) @@ -481,8 +490,13 @@ export const VoucherDetails: React.FC = ({ confirmButtonState={ voucherCataloguesAddOpts.status } + hasMore={ + searchProductsOpts.data?.search.pageInfo + .hasNextPage + } open={params.action === "assign-product"} onFetch={searchProducts} + onFetchMore={loadMoreProducts} loading={searchProductsOpts.loading} onClose={closeModal} onSubmit={products => diff --git a/src/storybook/stories/components/AssignProductDialog.tsx b/src/storybook/stories/components/AssignProductDialog.tsx index 8898dcf56..27ce8df54 100644 --- a/src/storybook/stories/components/AssignProductDialog.tsx +++ b/src/storybook/stories/components/AssignProductDialog.tsx @@ -2,6 +2,7 @@ import placeholderImage from "@assets/images/placeholder60x60.png"; import AssignProductDialog, { AssignProductDialogProps } from "@saleor/components/AssignProductDialog"; +import { fetchMoreProps } from "@saleor/fixtures"; import { products } from "@saleor/products/fixtures"; import { storiesOf } from "@storybook/react"; import React from "react"; @@ -9,6 +10,7 @@ import React from "react"; import Decorator from "../../Decorator"; const props: AssignProductDialogProps = { + ...fetchMoreProps, confirmButtonState: "default", loading: false, onClose: () => undefined, diff --git a/src/styles/useScrollableDialogStyle.ts b/src/styles/useScrollableDialogStyle.ts new file mode 100644 index 000000000..7b9a65a9a --- /dev/null +++ b/src/styles/useScrollableDialogStyle.ts @@ -0,0 +1,28 @@ +import makeStyles from "@material-ui/core/styles/makeStyles"; + +const useScrollableDialogStyle = makeStyles( + theme => ({ + content: { + overflowY: "hidden" + }, + dialog: { + height: "calc(100% - 64px)", + maxHeight: 700 + }, + loadMoreLoaderContainer: { + alignItems: "center", + display: "flex", + height: theme.spacing(3), + justifyContent: "center", + marginTop: theme.spacing(3) + }, + scrollArea: { + overflowY: "scroll" + } + }), + { + name: "ScrollableDialog" + } +); + +export default useScrollableDialogStyle;