diff --git a/src/categories/queries.ts b/src/categories/queries.ts index 4bad812fe..d8308b92d 100644 --- a/src/categories/queries.ts +++ b/src/categories/queries.ts @@ -1,6 +1,7 @@ import gql from "graphql-tag"; -import { pageInfoFragment, TypedQuery } from "../queries"; +import makeQuery from "@saleor/hooks/makeQuery"; +import { pageInfoFragment } from "../queries"; import { CategoryDetails, CategoryDetailsVariables @@ -65,7 +66,7 @@ export const rootCategories = gql` } } `; -export const TypedRootCategoriesQuery = TypedQuery( +export const useRootCategoriesQuery = makeQuery( rootCategories ); @@ -119,7 +120,7 @@ export const categoryDetails = gql` } } `; -export const TypedCategoryDetailsQuery = TypedQuery< +export const useCategoryDetailsQuery = makeQuery< CategoryDetails, CategoryDetailsVariables >(categoryDetails); diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index 51046d603..5c6aa7d4d 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -28,7 +28,7 @@ import { useCategoryDeleteMutation, useCategoryUpdateMutation } from "../mutations"; -import { TypedCategoryDetailsQuery } from "../queries"; +import { useCategoryDetailsQuery } from "../queries"; import { CategoryBulkDelete } from "../types/CategoryBulkDelete"; import { CategoryDelete } from "../types/CategoryDelete"; import { CategoryUpdate } from "../types/CategoryUpdate"; @@ -63,6 +63,13 @@ export const CategoryDetails: React.FC = ({ ); const intl = useIntl(); + const paginationState = createPaginationState(PAGINATE_BY, params); + const { data, loading, refetch } = useCategoryDetailsQuery({ + displayLoader: true, + require: ["category"], + variables: { ...paginationState, id } + }); + const handleCategoryDelete = (data: CategoryDelete) => { if (data.categoryDelete.errors.length === 0) { notify({ @@ -140,7 +147,6 @@ export const CategoryDetails: React.FC = ({ }) ); - const paginationState = createPaginationState(PAGINATE_BY, params); const formTransitionState = getMutationState( updateResult.called, updateResult.loading, @@ -152,245 +158,221 @@ export const CategoryDetails: React.FC = ({ maybe(() => deleteResult.data.categoryDelete.errors) ); + const handleBulkProductDelete = (data: productBulkDelete) => { + if (data.productBulkDelete.errors.length === 0) { + closeModal(); + notify({ + text: intl.formatMessage(commonMessages.savedChanges) + }); + refetch(); + reset(); + } + }; + + const { loadNextPage, loadPreviousPage, pageInfo } = paginate( + params.activeTab === CategoryPageTab.categories + ? maybe(() => data.category.children.pageInfo) + : maybe(() => data.category.products.pageInfo), + paginationState, + params + ); + return ( - - {({ data, loading, refetch }) => { - const handleBulkProductDelete = (data: productBulkDelete) => { - if (data.productBulkDelete.errors.length === 0) { - closeModal(); - notify({ - text: intl.formatMessage(commonMessages.savedChanges) - }); - refetch(); - reset(); - } - }; + <> + data.category.name)} /> + + {(productBulkDelete, productBulkDeleteOpts) => { + const categoryBulkDeleteMutationState = getMutationState( + categoryBulkDeleteOpts.called, + categoryBulkDeleteOpts.loading, + maybe(() => categoryBulkDeleteOpts.data.categoryBulkDelete.errors) + ); + const productBulkDeleteMutationState = getMutationState( + productBulkDeleteOpts.called, + productBulkDeleteOpts.loading, + maybe(() => productBulkDeleteOpts.data.productBulkDelete.errors) + ); - const { loadNextPage, loadPreviousPage, pageInfo } = paginate( - params.activeTab === CategoryPageTab.categories - ? maybe(() => data.category.children.pageInfo) - : maybe(() => data.category.products.pageInfo), - paginationState, - params - ); - - return ( - <> - data.category.name)} /> - - {(productBulkDelete, productBulkDeleteOpts) => { - const categoryBulkDeleteMutationState = getMutationState( - categoryBulkDeleteOpts.called, - categoryBulkDeleteOpts.loading, - maybe( - () => categoryBulkDeleteOpts.data.categoryBulkDelete.errors + return ( + <> + data.category)} + disabled={loading} + errors={maybe(() => updateResult.data.categoryUpdate.errors)} + onAddCategory={() => navigate(categoryAddUrl(id))} + onAddProduct={() => navigate(productAddUrl)} + onBack={() => + navigate( + maybe( + () => categoryUrl(data.category.parent.id), + categoryListUrl() + ) ) - ); - const productBulkDeleteMutationState = getMutationState( - productBulkDeleteOpts.called, - productBulkDeleteOpts.loading, - maybe( - () => productBulkDeleteOpts.data.productBulkDelete.errors - ) - ); - - return ( - <> - data.category)} - disabled={loading} - errors={maybe( - () => updateResult.data.categoryUpdate.errors - )} - onAddCategory={() => navigate(categoryAddUrl(id))} - onAddProduct={() => navigate(productAddUrl)} - onBack={() => - navigate( - maybe( - () => categoryUrl(data.category.parent.id), - categoryListUrl() - ) - ) + } + onCategoryClick={id => () => navigate(categoryUrl(id))} + onDelete={() => openModal("delete")} + onImageDelete={() => + updateCategory({ + variables: { + id, + input: { + backgroundImage: null } - onCategoryClick={id => () => navigate(categoryUrl(id))} - onDelete={() => openModal("delete")} - onImageDelete={() => - updateCategory({ - variables: { - id, - input: { - backgroundImage: null - } - } - }) + } + }) + } + onImageUpload={file => + updateCategory({ + variables: { + id, + input: { + backgroundImage: file } - onImageUpload={file => - updateCategory({ - variables: { - id, - input: { - backgroundImage: file - } - } - }) + } + }) + } + onNextPage={loadNextPage} + onPreviousPage={loadPreviousPage} + pageInfo={pageInfo} + onProductClick={id => () => navigate(productUrl(id))} + onSubmit={formData => + updateCategory({ + variables: { + id, + input: { + backgroundImageAlt: formData.backgroundImageAlt, + descriptionJson: JSON.stringify(formData.description), + name: formData.name, + seo: { + description: formData.seoDescription, + title: formData.seoTitle + } } - onNextPage={loadNextPage} - onPreviousPage={loadPreviousPage} - pageInfo={pageInfo} - onProductClick={id => () => navigate(productUrl(id))} - onSubmit={formData => - updateCategory({ - variables: { - id, - input: { - backgroundImageAlt: formData.backgroundImageAlt, - descriptionJson: JSON.stringify( - formData.description - ), - name: formData.name, - seo: { - description: formData.seoDescription, - title: formData.seoTitle - } - } - } - }) - } - products={maybe(() => - data.category.products.edges.map(edge => edge.node) - )} - saveButtonBarState={formTransitionState} - subcategories={maybe(() => - data.category.children.edges.map(edge => edge.node) - )} - subcategoryListToolbar={ - - openModal("delete-categories", listElements) - } - > - - - } - productListToolbar={ - - openModal("delete-products", listElements) - } - > - - - } - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} - /> - deleteCategory({ variables: { id } })} - open={params.action === "delete"} - title={intl.formatMessage({ - defaultMessage: "Delete category", - description: "dialog title" - })} - variant="delete" - > - - - {maybe(() => data.category.name, "...")} - - ) - }} - /> - - - - - - params.ids.length > 0) - } - confirmButtonState={categoryBulkDeleteMutationState} - onClose={closeModal} - onConfirm={() => - categoryBulkDelete({ - variables: { ids: params.ids } - }).then(() => refetch()) - } - title={intl.formatMessage({ - defaultMessage: "Delete categories", - description: "dialog title" - })} - variant="delete" - > - - params.ids.length), - displayQuantity: ( - {maybe(() => params.ids.length)} - ) - }} - /> - - - - - - - productBulkDelete({ - variables: { ids: params.ids } - }).then(() => refetch()) - } - title={intl.formatMessage({ - defaultMessage: "Delete products", - description: "dialog title" - })} - variant="delete" - > - - params.ids.length), - displayQuantity: ( - {maybe(() => params.ids.length)} - ) - }} - /> - - - - ); - }} - - - ); - }} - + } + }) + } + products={maybe(() => + data.category.products.edges.map(edge => edge.node) + )} + saveButtonBarState={formTransitionState} + subcategories={maybe(() => + data.category.children.edges.map(edge => edge.node) + )} + subcategoryListToolbar={ + openModal("delete-categories", listElements)} + > + + + } + productListToolbar={ + openModal("delete-products", listElements)} + > + + + } + isChecked={isSelected} + selected={listElements.length} + toggle={toggle} + toggleAll={toggleAll} + /> + deleteCategory({ variables: { id } })} + open={params.action === "delete"} + title={intl.formatMessage({ + defaultMessage: "Delete category", + description: "dialog title" + })} + variant="delete" + > + + + {maybe(() => data.category.name, "...")} + + ) + }} + /> + + + + + + params.ids.length > 0) + } + confirmButtonState={categoryBulkDeleteMutationState} + onClose={closeModal} + onConfirm={() => + categoryBulkDelete({ + variables: { ids: params.ids } + }).then(() => refetch()) + } + title={intl.formatMessage({ + defaultMessage: "Delete categories", + description: "dialog title" + })} + variant="delete" + > + + params.ids.length), + displayQuantity: ( + {maybe(() => params.ids.length)} + ) + }} + /> + + + + + + + productBulkDelete({ + variables: { ids: params.ids } + }).then(() => refetch()) + } + title={intl.formatMessage({ + defaultMessage: "Delete products", + description: "dialog title" + })} + variant="delete" + > + + params.ids.length), + displayQuantity: ( + {maybe(() => params.ids.length)} + ) + }} + /> + + + + ); + }} + + ); }; export default CategoryDetails; diff --git a/src/categories/views/CategoryList/CategoryList.tsx b/src/categories/views/CategoryList/CategoryList.tsx index e0b4ee657..ac38342a7 100644 --- a/src/categories/views/CategoryList/CategoryList.tsx +++ b/src/categories/views/CategoryList/CategoryList.tsx @@ -19,7 +19,7 @@ import { getMutationState, maybe } from "@saleor/misc"; import { ListViews } from "@saleor/types"; import { CategoryListPage } from "../../components/CategoryListPage/CategoryListPage"; import { useCategoryBulkDeleteMutation } from "../../mutations"; -import { TypedRootCategoriesQuery } from "../../queries"; +import { useRootCategoriesQuery } from "../../queries"; import { CategoryBulkDelete } from "../../types/CategoryBulkDelete"; import { categoryAddUrl, @@ -53,6 +53,20 @@ export const CategoryList: React.FC = ({ params }) => { ); const intl = useIntl(); + const paginationState = createPaginationState(settings.rowNumber, params); + const queryVariables = React.useMemo( + () => ({ + ...paginationState, + filter: getFilterVariables(params) + }), + [params] + ); + const { data, loading, refetch } = useRootCategoriesQuery({ + displayLoader: true, + require: ["categories"], + variables: queryVariables + }); + const tabs = getFilterTabs(); const currentTab = @@ -113,146 +127,129 @@ export const CategoryList: React.FC = ({ params }) => { handleTabChange(tabs.length + 1); }; - const paginationState = createPaginationState(settings.rowNumber, params); - const queryVariables = React.useMemo( - () => ({ - ...paginationState, - filter: getFilterVariables(params) - }), - [params] + const { loadNextPage, loadPreviousPage, pageInfo } = paginate( + maybe(() => data.categories.pageInfo), + paginationState, + params + ); + + const handleCategoryBulkDelete = (data: CategoryBulkDelete) => { + if (data.categoryBulkDelete.errors.length === 0) { + navigate(categoryListUrl(), true); + refetch(); + reset(); + } + }; + + const [ + categoryBulkDelete, + categoryBulkDeleteOpts + ] = useCategoryBulkDeleteMutation({ + onCompleted: handleCategoryBulkDelete + }); + + const bulkDeleteState = getMutationState( + categoryBulkDeleteOpts.called, + categoryBulkDeleteOpts.loading, + maybe(() => categoryBulkDeleteOpts.data.categoryBulkDelete.errors) ); return ( - - {({ data, loading, refetch }) => { - const { loadNextPage, loadPreviousPage, pageInfo } = paginate( - maybe(() => data.categories.pageInfo), - paginationState, - params - ); - - const handleCategoryBulkDelete = (data: CategoryBulkDelete) => { - if (data.categoryBulkDelete.errors.length === 0) { - navigate(categoryListUrl(), true); - refetch(); - reset(); - } - }; - - const [ - categoryBulkDelete, - categoryBulkDeleteOpts - ] = useCategoryBulkDeleteMutation({ - onCompleted: handleCategoryBulkDelete - }); - - const bulkDeleteState = getMutationState( - categoryBulkDeleteOpts.called, - categoryBulkDeleteOpts.loading, - maybe(() => categoryBulkDeleteOpts.data.categoryBulkDelete.errors) - ); - - return ( - <> - data.categories.edges.map(edge => edge.node), - [] - )} - currentTab={currentTab} - initialSearch={params.query || ""} - onSearchChange={query => changeFilterField({ query })} - onAll={() => navigate(categoryListUrl())} - onTabChange={handleTabChange} - onTabDelete={() => openModal("delete-search")} - onTabSave={() => openModal("save-search")} - tabs={tabs.map(tab => tab.name)} - settings={settings} - onAdd={() => navigate(categoryAddUrl())} - onRowClick={id => () => navigate(categoryUrl(id))} - disabled={loading} - onNextPage={loadNextPage} - onPreviousPage={loadPreviousPage} - onUpdateListSettings={updateListSettings} - pageInfo={pageInfo} - isChecked={isSelected} - selected={listElements.length} - toggle={toggle} - toggleAll={toggleAll} - toolbar={ - - navigate( - categoryListUrl({ - ...params, - action: "delete", - ids: listElements - }) - ) - } - > - - - } - /> - - navigate( - categoryListUrl({ - ...params, - action: undefined, - ids: undefined - }) - ) - } - onConfirm={() => - categoryBulkDelete({ - variables: { - ids: params.ids - } + <> + data.categories.edges.map(edge => edge.node), + [] + )} + currentTab={currentTab} + initialSearch={params.query || ""} + onSearchChange={query => changeFilterField({ query })} + onAll={() => navigate(categoryListUrl())} + onTabChange={handleTabChange} + onTabDelete={() => openModal("delete-search")} + onTabSave={() => openModal("save-search")} + tabs={tabs.map(tab => tab.name)} + settings={settings} + onAdd={() => navigate(categoryAddUrl())} + onRowClick={id => () => navigate(categoryUrl(id))} + disabled={loading} + onNextPage={loadNextPage} + onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} + pageInfo={pageInfo} + isChecked={isSelected} + selected={listElements.length} + toggle={toggle} + toggleAll={toggleAll} + toolbar={ + + navigate( + categoryListUrl({ + ...params, + action: "delete", + ids: listElements }) - } - open={params.action === "delete"} - title={intl.formatMessage({ - defaultMessage: "Delete categories", - description: "dialog title" - })} - variant="delete" - > - - params.ids.length), - displayQuantity: ( - {maybe(() => params.ids.length)} - ) - }} - /> - - - - - - - tabs[currentTab - 1].name, "...")} - /> - - ); - }} - + ) + } + > + + + } + /> + + navigate( + categoryListUrl({ + ...params, + action: undefined, + ids: undefined + }) + ) + } + onConfirm={() => + categoryBulkDelete({ + variables: { + ids: params.ids + } + }) + } + open={params.action === "delete"} + title={intl.formatMessage({ + defaultMessage: "Delete categories", + description: "dialog title" + })} + variant="delete" + > + + params.ids.length), + displayQuantity: {maybe(() => params.ids.length)} + }} + /> + + + + + + + tabs[currentTab - 1].name, "...")} + /> + ); }; export default CategoryList; diff --git a/src/hooks/makeQuery.ts b/src/hooks/makeQuery.ts new file mode 100644 index 000000000..a16909766 --- /dev/null +++ b/src/hooks/makeQuery.ts @@ -0,0 +1,115 @@ +import { ApolloQueryResult } from "apollo-client"; +import { DocumentNode } from "graphql"; +import { useEffect } from "react"; +import { QueryResult, useQuery as useBaseQuery } from "react-apollo"; +import { useIntl } from "react-intl"; + +import { commonMessages } from "@saleor/intl"; +import { maybe, RequireAtLeastOne } from "@saleor/misc"; +import useAppState from "./useAppState"; +import useNotifier from "./useNotifier"; + +export interface LoadMore { + loadMore: ( + mergeFunc: (prev: TData, next: TData) => TData, + extraVariables: Partial + ) => Promise>; +} + +type UseQuery = QueryResult & + LoadMore; +type UseQueryOpts = Partial<{ + displayLoader: boolean; + require: Array; + skip: boolean; + variables: TVariables; +}>; +type UseQueryHook = ( + opts: UseQueryOpts +) => UseQuery; + +function makeQuery( + query: DocumentNode +): UseQueryHook { + function useQuery({ + displayLoader, + require, + skip, + variables + }: UseQueryOpts): UseQuery { + const notify = useNotifier(); + const intl = useIntl(); + const [, dispatchAppState] = useAppState(); + const queryData = useBaseQuery(query, { + context: { + useBatching: true + }, + errorPolicy: "all", + fetchPolicy: "cache-and-network", + skip, + variables + }); + + useEffect(() => { + if (displayLoader) { + dispatchAppState({ + payload: { + value: queryData.loading + }, + type: "displayLoader" + }); + } + }, [queryData.loading]); + + if (queryData.error) { + if ( + !queryData.error.graphQLErrors.every( + err => + maybe(() => err.extensions.exception.code) === "PermissionDenied" + ) + ) { + notify({ + text: intl.formatMessage(commonMessages.somethingWentWrong) + }); + } + } + + const loadMore = ( + mergeFunc: (previousResults: TData, fetchMoreResult: TData) => TData, + extraVariables: RequireAtLeastOne + ) => + queryData.fetchMore({ + query, + updateQuery: (previousResults, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return previousResults; + } + return mergeFunc(previousResults, fetchMoreResult); + }, + variables: { ...variables, ...extraVariables } + }); + + if ( + !queryData.loading && + require && + queryData.data && + !require.reduce((acc, key) => acc && queryData.data[key] !== null, true) + ) { + dispatchAppState({ + payload: { + error: "not-found" + }, + type: "displayError" + }); + } + + return { + ...queryData, + loadMore + }; + } + + return useQuery; +} + +export default makeQuery;