From 1dd0488c62d935324c17fcb10479b2331b2e49aa Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 13 Nov 2019 17:46:08 +0100 Subject: [PATCH 1/7] Use mutation hooks --- package.json | 2 +- src/categories/mutations.ts | 10 +- src/categories/views/CategoryCreate.tsx | 97 ++- src/categories/views/CategoryDetails.tsx | 598 ++++++++---------- .../views/CategoryList/CategoryList.tsx | 224 ++++--- src/hooks/makeMutation.ts | 65 ++ 6 files changed, 506 insertions(+), 490 deletions(-) create mode 100644 src/hooks/makeMutation.ts diff --git a/package.json b/package.json index e0895b046..31d82bf87 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "moment-timezone": "^0.5.26", "qs": "^6.9.0", "react": "^16.9.0", - "react-apollo": "^3.0.0", + "react-apollo": "^3.1.3", "react-dom": "^16.9.0", "react-dropzone": "^8.2.0", "react-error-boundary": "^1.2.5", diff --git a/src/categories/mutations.ts b/src/categories/mutations.ts index 715aa4bbf..7995b3c6f 100644 --- a/src/categories/mutations.ts +++ b/src/categories/mutations.ts @@ -1,6 +1,6 @@ import gql from "graphql-tag"; -import { TypedMutation } from "../mutations"; +import makeMutation from "@saleor/hooks/makeMutation"; import { categoryDetailsFragment } from "./queries"; import { CategoryBulkDelete, @@ -29,7 +29,7 @@ export const categoryDeleteMutation = gql` } } `; -export const TypedCategoryDeleteMutation = TypedMutation< +export const useCategoryDeleteMutation = makeMutation< CategoryDelete, CategoryDeleteVariables >(categoryDeleteMutation); @@ -48,7 +48,7 @@ export const categoryCreateMutation = gql` } } `; -export const TypedCategoryCreateMutation = TypedMutation< +export const useCategoryCreateMutation = makeMutation< CategoryCreate, CategoryCreateVariables >(categoryCreateMutation); @@ -67,7 +67,7 @@ export const categoryUpdateMutation = gql` } } `; -export const TypedCategoryUpdateMutation = TypedMutation< +export const useCategoryUpdateMutation = makeMutation< CategoryUpdate, CategoryUpdateVariables >(categoryUpdateMutation); @@ -82,7 +82,7 @@ export const categoryBulkDeleteMutation = gql` } } `; -export const TypedCategoryBulkDeleteMutation = TypedMutation< +export const useCategoryBulkDeleteMutation = makeMutation< CategoryBulkDelete, CategoryBulkDeleteVariables >(categoryBulkDeleteMutation); diff --git a/src/categories/views/CategoryCreate.tsx b/src/categories/views/CategoryCreate.tsx index 90d1df35d..592552c00 100644 --- a/src/categories/views/CategoryCreate.tsx +++ b/src/categories/views/CategoryCreate.tsx @@ -6,7 +6,7 @@ import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { getMutationState, maybe } from "../../misc"; import CategoryCreatePage from "../components/CategoryCreatePage"; -import { TypedCategoryCreateMutation } from "../mutations"; +import { useCategoryCreateMutation } from "../mutations"; import { CategoryCreate } from "../types/CategoryCreate"; import { categoryListUrl, categoryUrl } from "../urls"; @@ -31,55 +31,54 @@ export const CategoryCreateView: React.FC = ({ navigate(categoryUrl(data.categoryCreate.category.id)); } }; + + const [createCategory, createCategoryResult] = useCategoryCreateMutation({ + onCompleted: handleSuccess + }); + + const errors = maybe( + () => createCategoryResult.data.categoryCreate.errors, + [] + ); + + const formTransitionState = getMutationState( + createCategoryResult.called, + createCategoryResult.loading, + errors + ); + return ( - - {(createCategory, createCategoryResult) => { - const errors = maybe( - () => createCategoryResult.data.categoryCreate.errors, - [] - ); - - const formTransitionState = getMutationState( - createCategoryResult.called, - createCategoryResult.loading, - errors - ); - - return ( - <> - - - navigate(parentId ? categoryUrl(parentId) : categoryListUrl()) - } - onSubmit={formData => - createCategory({ - variables: { - input: { - descriptionJson: JSON.stringify(formData.description), - name: formData.name, - seo: { - description: formData.seoDescription, - title: formData.seoTitle - } - }, - parent: parentId || null - } - }) - } - /> - - ); - }} - + <> + + + navigate(parentId ? categoryUrl(parentId) : categoryListUrl()) + } + onSubmit={formData => + createCategory({ + variables: { + input: { + descriptionJson: JSON.stringify(formData.description), + name: formData.name, + seo: { + description: formData.seoDescription, + title: formData.seoTitle + } + }, + parent: parentId || null + } + }) + } + /> + ); }; export default CategoryCreateView; diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index 8a4cf6002..51046d603 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -24,9 +24,9 @@ import { CategoryUpdatePage } from "../components/CategoryUpdatePage/CategoryUpdatePage"; import { - TypedCategoryBulkDeleteMutation, - TypedCategoryDeleteMutation, - TypedCategoryUpdateMutation + useCategoryBulkDeleteMutation, + useCategoryDeleteMutation, + useCategoryUpdateMutation } from "../mutations"; import { TypedCategoryDetailsQuery } from "../queries"; import { CategoryBulkDelete } from "../types/CategoryBulkDelete"; @@ -73,6 +73,11 @@ export const CategoryDetails: React.FC = ({ navigate(categoryListUrl()); } }; + + const [deleteCategory, deleteResult] = useCategoryDeleteMutation({ + onCompleted: handleCategoryDelete + }); + const handleCategoryUpdate = (data: CategoryUpdate) => { if (data.categoryUpdate.errors.length > 0) { const backgroundImageError = data.categoryUpdate.errors.find( @@ -86,6 +91,27 @@ export const CategoryDetails: React.FC = ({ } }; + const [updateCategory, updateResult] = useCategoryUpdateMutation({ + onCompleted: handleCategoryUpdate + }); + + const handleBulkCategoryDelete = (data: CategoryBulkDelete) => { + if (data.categoryBulkDelete.errors.length === 0) { + closeModal(); + notify({ + text: intl.formatMessage(commonMessages.savedChanges) + }); + reset(); + } + }; + + const [ + categoryBulkDelete, + categoryBulkDeleteOpts + ] = useCategoryBulkDeleteMutation({ + onCompleted: handleBulkCategoryDelete + }); + const changeTab = (tabName: CategoryPageTab) => { reset(); navigate( @@ -114,329 +140,257 @@ export const CategoryDetails: React.FC = ({ }) ); + const paginationState = createPaginationState(PAGINATE_BY, params); + const formTransitionState = getMutationState( + updateResult.called, + updateResult.loading, + maybe(() => updateResult.data.categoryUpdate.errors) + ); + const removeDialogTransitionState = getMutationState( + deleteResult.called, + deleteResult.loading, + maybe(() => deleteResult.data.categoryDelete.errors) + ); + return ( - - {(deleteCategory, deleteResult) => ( - - {(updateCategory, updateResult) => { - const paginationState = createPaginationState(PAGINATE_BY, params); - const formTransitionState = getMutationState( - updateResult.called, - updateResult.loading, - maybe(() => updateResult.data.categoryUpdate.errors) - ); - const removeDialogTransitionState = getMutationState( - deleteResult.called, - deleteResult.loading, - maybe(() => deleteResult.data.categoryDelete.errors) - ); - return ( - - {({ data, loading, refetch }) => { - const handleBulkCategoryDelete = ( - data: CategoryBulkDelete - ) => { - if (data.categoryBulkDelete.errors.length === 0) { - closeModal(); - notify({ - text: intl.formatMessage(commonMessages.savedChanges) - }); - refetch(); - reset(); - } - }; + + {({ data, loading, refetch }) => { + const handleBulkProductDelete = (data: productBulkDelete) => { + if (data.productBulkDelete.errors.length === 0) { + closeModal(); + notify({ + text: intl.formatMessage(commonMessages.savedChanges) + }); + refetch(); + reset(); + } + }; - 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 + ); - 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 + ) + ); + const productBulkDeleteMutationState = getMutationState( + productBulkDeleteOpts.called, + productBulkDeleteOpts.loading, + maybe( + () => productBulkDeleteOpts.data.productBulkDelete.errors + ) + ); - return ( - <> - data.category.name)} /> - - {(categoryBulkDelete, categoryBulkDeleteOpts) => ( - - {(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 - ) - ); - - 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 - } - } - }) - } - 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 - } - } - } - }) - } - 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 } - }) - } - title={intl.formatMessage({ - defaultMessage: "Delete categories", - description: "dialog title" - })} - variant="delete" - > - - params.ids.length - ), - displayQuantity: ( - - {maybe(() => params.ids.length)} - - ) - }} - /> - - - - - - - productBulkDelete({ - variables: { ids: params.ids } - }) - } - title={intl.formatMessage({ - defaultMessage: "Delete products", - description: "dialog title" - })} - variant="delete" - > - {" "} - - params.ids.length - ), - displayQuantity: ( - - {maybe(() => params.ids.length)} - - ) - }} - /> - - - - ); - }} - - )} - - - ); - }} - - ); - }} - - )} - + 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 + } + } + }) + } + 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 + } + } + } + }) + } + 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 d3ec14a44..e0b4ee657 100644 --- a/src/categories/views/CategoryList/CategoryList.tsx +++ b/src/categories/views/CategoryList/CategoryList.tsx @@ -18,7 +18,7 @@ import usePaginator, { import { getMutationState, maybe } from "@saleor/misc"; import { ListViews } from "@saleor/types"; import { CategoryListPage } from "../../components/CategoryListPage/CategoryListPage"; -import { TypedCategoryBulkDeleteMutation } from "../../mutations"; +import { useCategoryBulkDeleteMutation } from "../../mutations"; import { TypedRootCategoriesQuery } from "../../queries"; import { CategoryBulkDelete } from "../../types/CategoryBulkDelete"; import { @@ -138,120 +138,118 @@ export const CategoryList: React.FC = ({ params }) => { reset(); } }; - return ( - - {(categoryBulkDelete, categoryBulkDeleteOpts) => { - 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 - } + 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 }) - } - 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, "...")} + /> + ); }} diff --git a/src/hooks/makeMutation.ts b/src/hooks/makeMutation.ts new file mode 100644 index 000000000..b6cfd4a91 --- /dev/null +++ b/src/hooks/makeMutation.ts @@ -0,0 +1,65 @@ +import { ApolloError } from "apollo-client"; +import { DocumentNode } from "graphql"; +import { + MutationFunction, + MutationResult, + useMutation as useBaseMutation +} from "react-apollo"; +import { useIntl } from "react-intl"; + +import { commonMessages } from "@saleor/intl"; +import { maybe } from "@saleor/misc"; +import useNotifier from "./useNotifier"; + +type UseMutation = [ + MutationFunction, + MutationResult +]; +type UseMutationCbs = Partial<{ + onCompleted: (data: TData) => void; + onError: (error: ApolloError) => void; +}>; +type UseMutationHook = ( + cbs: UseMutationCbs +) => UseMutation; + +function makeMutation( + mutation: DocumentNode +): UseMutationHook { + function useMutation({ + onCompleted, + onError + }: UseMutationCbs): UseMutation { + const notify = useNotifier(); + const intl = useIntl(); + const [mutateFn, result] = useBaseMutation(mutation, { + onCompleted, + onError: (err: ApolloError) => { + if ( + maybe( + () => + err.graphQLErrors[0].extensions.exception.code === + "ReadOnlyException" + ) + ) { + notify({ + text: intl.formatMessage(commonMessages.readOnly) + }); + } else { + notify({ + text: intl.formatMessage(commonMessages.somethingWentWrong) + }); + } + if (onError) { + onError(err); + } + } + }); + + return [mutateFn, result]; + } + + return useMutation; +} + +export default makeMutation; From 0d51418f726b216f77494a38c0b563fcbab61864 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 13 Nov 2019 17:52:46 +0100 Subject: [PATCH 2/7] Display not found instead of error --- src/queries.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/queries.tsx b/src/queries.tsx index 4b259b587..bd71dc233 100644 --- a/src/queries.tsx +++ b/src/queries.tsx @@ -6,7 +6,7 @@ import { Query, QueryResult } from "react-apollo"; import { useIntl } from "react-intl"; import AppProgress from "./components/AppProgress"; -import ErrorPage from "./components/ErrorPage/ErrorPage"; +import NotFoundPage from "./components/NotFoundPage"; import useNavigator from "./hooks/useNavigator"; import useNotifier from "./hooks/useNotifier"; import { commonMessages } from "./intl"; @@ -129,7 +129,9 @@ export function TypedQuery( true ) ) { - childrenOrNotFound = navigate("/")} />; + childrenOrNotFound = ( + navigate("/")} /> + ); } if (displayLoader) { From b12f3c35faa08fe8746515a7d0809d8f9d6e02b3 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 14 Nov 2019 15:10:52 +0100 Subject: [PATCH 3/7] Add app state --- src/auth/components/SectionRoute.tsx | 18 +- src/components/AppLayout/AppLayout.tsx | 432 ++++++++++--------- src/components/AppProgress/index.tsx | 28 -- src/components/ErrorPage/ErrorPage.tsx | 2 +- src/components/NotFoundPage/NotFoundPage.tsx | 3 +- src/containers/AppState/AppState.tsx | 40 ++ src/containers/AppState/index.ts | 2 + src/containers/AppState/reducer.ts | 42 ++ src/containers/AppState/state.ts | 13 + src/hooks/useAppState.ts | 11 + src/index.tsx | 231 +++++----- src/queries.tsx | 171 ++++---- 12 files changed, 551 insertions(+), 442 deletions(-) delete mode 100644 src/components/AppProgress/index.tsx create mode 100644 src/containers/AppState/AppState.tsx create mode 100644 src/containers/AppState/index.ts create mode 100644 src/containers/AppState/reducer.ts create mode 100644 src/containers/AppState/state.ts create mode 100644 src/hooks/useAppState.ts diff --git a/src/auth/components/SectionRoute.tsx b/src/auth/components/SectionRoute.tsx index 840ae2f89..f1b2d1546 100644 --- a/src/auth/components/SectionRoute.tsx +++ b/src/auth/components/SectionRoute.tsx @@ -1,10 +1,6 @@ import React from "react"; -import ErrorBoundary from "react-error-boundary"; import { Route, RouteProps } from "react-router-dom"; -import AppLayout from "@saleor/components/AppLayout"; -import ErrorPage from "@saleor/components/ErrorPage"; -import useNavigator from "@saleor/hooks/useNavigator"; import useUser from "@saleor/hooks/useUser"; import NotFound from "../../NotFound"; import { PermissionEnum } from "../../types/globalTypes"; @@ -18,7 +14,6 @@ export const SectionRoute: React.FC = ({ permissions, ...props }) => { - const navigate = useNavigator(); const { user } = useUser(); const hasPermissions = @@ -26,18 +21,7 @@ export const SectionRoute: React.FC = ({ permissions .map(permission => hasPermission(permission, user)) .reduce((prev, curr) => prev && curr); - return hasPermissions ? ( - - navigate("/")} />} - key={permissions ? permissions.join(":") : "home"} - > - - - - ) : ( - - ); + return hasPermissions ? : ; }; SectionRoute.displayName = "Route"; export default SectionRoute; diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index bacdc3954..0362dc578 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -13,13 +13,13 @@ import classNames from "classnames"; import React from "react"; import SVG from "react-inlinesvg"; import { FormattedMessage, useIntl } from "react-intl"; -import { RouteComponentProps, withRouter } from "react-router"; +import useRouter from "use-react-router"; import saleorDarkLogoSmall from "@assets/images/logo-dark-small.svg"; import saleorDarkLogo from "@assets/images/logo-dark.svg"; import menuArrowIcon from "@assets/images/menu-arrow-icon.svg"; -import AppProgressProvider from "@saleor/components/AppProgress"; import { createConfigurationMenu } from "@saleor/configuration"; +import useAppState from "@saleor/hooks/useAppState"; import useLocalStorage from "@saleor/hooks/useLocalStorage"; import useNavigator from "@saleor/hooks/useNavigator"; import useTheme from "@saleor/hooks/useTheme"; @@ -28,6 +28,8 @@ import ArrowDropdown from "@saleor/icons/ArrowDropdown"; import { maybe } from "@saleor/misc"; import { staffMemberDetailsUrl } from "@saleor/staff/urls"; import Container from "../Container"; +import ErrorPage from "../ErrorPage"; +import NotFoundPage from "../NotFoundPage"; import AppActionContext from "./AppActionContext"; import AppHeaderContext from "./AppHeaderContext"; import { appLoaderHeight, drawerWidth, drawerWidthExpanded } from "./consts"; @@ -273,222 +275,230 @@ interface AppLayoutProps { children: React.ReactNode; } -const AppLayout = withRouter, any>( - ({ children, location }: AppLayoutProps & RouteComponentProps) => { - const classes = useStyles({}); - const { isDark, toggleTheme } = useTheme(); - const [isMenuSmall, setMenuSmall] = useLocalStorage("isMenuSmall", false); - const [isDrawerOpened, setDrawerState] = React.useState(false); - const [isMenuOpened, setMenuState] = React.useState(false); - const appActionAnchor = React.useRef(); - const appHeaderAnchor = React.useRef(); - const anchor = React.useRef(); - const { logout, user } = useUser(); - const navigate = useNavigator(); - const intl = useIntl(); +const AppLayout: React.FC = ({ children }) => { + const classes = useStyles({}); + const { isDark, toggleTheme } = useTheme(); + const [isMenuSmall, setMenuSmall] = useLocalStorage("isMenuSmall", false); + const [isDrawerOpened, setDrawerState] = React.useState(false); + const [isMenuOpened, setMenuState] = React.useState(false); + const appActionAnchor = React.useRef(); + const appHeaderAnchor = React.useRef(); + const anchor = React.useRef(); + const { logout, user } = useUser(); + const navigate = useNavigator(); + const intl = useIntl(); + const [appState, dispatchAppState] = useAppState(); + const { location } = useRouter(); - const menuStructure = createMenuStructure(intl); - const configurationMenu = createConfigurationMenu(intl); - const userPermissions = maybe(() => user.permissions, []); + const menuStructure = createMenuStructure(intl); + const configurationMenu = createConfigurationMenu(intl); + const userPermissions = maybe(() => user.permissions, []); - const renderConfigure = configurationMenu.some(section => - section.menuItems.some( - menuItem => - !!userPermissions.find( - userPermission => userPermission.code === menuItem.permission - ) - ) - ); + const renderConfigure = configurationMenu.some(section => + section.menuItems.some( + menuItem => + !!userPermissions.find( + userPermission => userPermission.code === menuItem.permission + ) + ) + ); - const handleLogout = () => { - setMenuState(false); - logout(); - }; + const handleLogout = () => { + setMenuState(false); + logout(); + }; - const handleViewerProfile = () => { - setMenuState(false); - navigate(staffMemberDetailsUrl(user.id)); - }; + const handleViewerProfile = () => { + setMenuState(false); + navigate(staffMemberDetailsUrl(user.id)); + }; - const handleMenuItemClick = (url: string, event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); - setDrawerState(false); - navigate(url); - }; + const handleMenuItemClick = (url: string, event: React.MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + setDrawerState(false); + navigate(url); + }; - const handleIsMenuSmall = () => { - setMenuSmall(!isMenuSmall); - }; + const handleIsMenuSmall = () => { + setMenuSmall(!isMenuSmall); + }; - return ( - - {({ isProgress }) => ( - - -
-
- setDrawerState(false)} - open={isDrawerOpened} - small={!isMenuSmall} - > -
- -
- -
- -
-
- -
-
-
- {isProgress ? ( - - ) : ( -
- )} -
-
- -
-
setDrawerState(!isDrawerOpened)} - > - - - - -
-
-
-
- -
- - ) - } - className={classes.userChip} - label={ - <> - {user.email} - - - } - onClick={() => setMenuState(!isMenuOpened)} - /> - - {({ TransitionProps, placement }) => ( - - - setMenuState(false)} - mouseEvent="onClick" - > - - - - - - - - - - - - )} - -
-
-
- -
-
{children}
-
-
-
+ const handleErrorBack = () => { + navigate("/"); + dispatchAppState({ + payload: { + error: null + }, + type: "displayError" + }); + }; + + return ( + + +
+
+ setDrawerState(false)} + open={isDrawerOpened} + small={!isMenuSmall} + > +
+
- - - )} - - ); - } -); + +
+ +
+
+ +
+
+
+ {appState.loading ? ( + + ) : ( +
+ )} +
+
+ +
+
setDrawerState(!isDrawerOpened)} + > + + + + +
+
+
+
+ +
+ + ) + } + className={classes.userChip} + label={ + <> + {user.email} + + + } + onClick={() => setMenuState(!isMenuOpened)} + /> + + {({ TransitionProps, placement }) => ( + + + setMenuState(false)} + mouseEvent="onClick" + > + + + + + + + + + + + + )} + +
+
+
+ +
+
+ {appState.error ? ( + appState.error === "not-found" ? ( + + ) : ( + + ) + ) : ( + children + )} +
+
+
+
+
+ + + ); +}; export default AppLayout; diff --git a/src/components/AppProgress/index.tsx b/src/components/AppProgress/index.tsx deleted file mode 100644 index 4ff8f199b..000000000 --- a/src/components/AppProgress/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; - -interface IAppProgressContext { - isProgress: boolean; - setProgressState: (isOpened: boolean) => void; -} - -export const AppProgressContext = React.createContext( - undefined -); - -export const AppProgressProvider: React.FC<{}> = ({ children }) => { - const [isProgress, setProgressState] = React.useState(false); - - return ( - - {children} - - ); -}; - -export const AppProgress = AppProgressContext.Consumer; -export default AppProgress; diff --git a/src/components/ErrorPage/ErrorPage.tsx b/src/components/ErrorPage/ErrorPage.tsx index 77b6ba041..51810fb5f 100644 --- a/src/components/ErrorPage/ErrorPage.tsx +++ b/src/components/ErrorPage/ErrorPage.tsx @@ -48,7 +48,7 @@ const useStyles = makeStyles(theme => ({ root: { alignItems: "center", display: "flex", - height: "calc(100vh - 88px)" + height: "calc(100vh - 180px)" }, upperHeader: { fontWeight: 600 as 600 diff --git a/src/components/NotFoundPage/NotFoundPage.tsx b/src/components/NotFoundPage/NotFoundPage.tsx index 3dc5d923f..a71160355 100644 --- a/src/components/NotFoundPage/NotFoundPage.tsx +++ b/src/components/NotFoundPage/NotFoundPage.tsx @@ -43,8 +43,7 @@ const useStyles = makeStyles(theme => ({ root: { alignItems: "center", display: "flex", - height: "100vh", - width: "100vw" + height: "calc(100vh - 180px)" } })); diff --git a/src/containers/AppState/AppState.tsx b/src/containers/AppState/AppState.tsx new file mode 100644 index 000000000..9f3b5a2cc --- /dev/null +++ b/src/containers/AppState/AppState.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import useRouter from "use-react-router"; + +import appStateReducer, { AppStateReducerAction } from "./reducer"; +import IAppState, { initialAppState } from "./state"; + +export type AppStateContextType = [ + IAppState, + React.Dispatch +]; +export const AppStateContext = React.createContext([ + initialAppState, + () => undefined +]); +const AppStateProvider: React.FC = ({ children }) => { + const { location } = useRouter(); + const stateAndDispatch = React.useReducer(appStateReducer, initialAppState); + const [state, dispatch] = stateAndDispatch; + + React.useEffect(() => { + if (!!state.error) { + dispatch({ + payload: { + error: null + }, + type: "displayError" + }); + } + }, [location]); + + return ( + + {children} + + ); +}; + +export const { Consumer } = AppStateContext; + +export default AppStateProvider; diff --git a/src/containers/AppState/index.ts b/src/containers/AppState/index.ts new file mode 100644 index 000000000..04b1b0a50 --- /dev/null +++ b/src/containers/AppState/index.ts @@ -0,0 +1,2 @@ +export { default } from "./AppState"; +export * from "./AppState"; diff --git a/src/containers/AppState/reducer.ts b/src/containers/AppState/reducer.ts new file mode 100644 index 000000000..1ed4654d4 --- /dev/null +++ b/src/containers/AppState/reducer.ts @@ -0,0 +1,42 @@ +import IAppState, { AppError } from "./state"; + +export type AppStateReducerActionType = "displayError" | "displayLoader"; + +export interface AppStateReducerAction { + payload: Partial<{ + error: AppError; + value: boolean; + }>; + type: AppStateReducerActionType; +} + +function displayError(prevState: IAppState, error: AppError): IAppState { + return { + ...prevState, + error, + loading: false + }; +} + +function displayLoader(prevState: IAppState, value: boolean): IAppState { + return { + ...prevState, + loading: value + }; +} + +function reduceAppState( + prevState: IAppState, + action: AppStateReducerAction +): IAppState { + switch (action.type) { + case "displayError": + return displayError(prevState, action.payload.error); + case "displayLoader": + return displayLoader(prevState, action.payload.value); + default: + return prevState; + } +} + +export default reduceAppState; diff --git a/src/containers/AppState/state.ts b/src/containers/AppState/state.ts new file mode 100644 index 000000000..efea07b71 --- /dev/null +++ b/src/containers/AppState/state.ts @@ -0,0 +1,13 @@ +export type AppError = "unhandled" | "not-found"; + +interface IAppState { + error: AppError | null; + loading: boolean; +} + +export const initialAppState: IAppState = { + error: null, + loading: false +}; + +export default IAppState; diff --git a/src/hooks/useAppState.ts b/src/hooks/useAppState.ts new file mode 100644 index 000000000..83abfa181 --- /dev/null +++ b/src/hooks/useAppState.ts @@ -0,0 +1,11 @@ +import React from "react"; + +import { AppStateContext } from "../containers/AppState"; + +function useAppState() { + const stateAndDispatch = React.useContext(AppStateContext); + + return stateAndDispatch; +} + +export default useAppState; diff --git a/src/index.tsx b/src/index.tsx index 7c193a445..b229ec5d1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,9 +8,11 @@ import { createUploadLink } from "apollo-upload-client"; import React from "react"; import { ApolloProvider } from "react-apollo"; import { render } from "react-dom"; +import ErrorBoundary from "react-error-boundary"; import { useIntl } from "react-intl"; import { BrowserRouter, Route, Switch } from "react-router-dom"; +import useAppState from "@saleor/hooks/useAppState"; import AttributeSection from "./attributes"; import { attributeSection } from "./attributes/urls"; import Auth, { getAuthToken, removeAuthToken } from "./auth"; @@ -20,7 +22,7 @@ import SectionRoute from "./auth/components/SectionRoute"; import { hasPermission } from "./auth/misc"; import CategorySection from "./categories"; import CollectionSection from "./collections"; -import { AppProgressProvider } from "./components/AppProgress"; +import AppLayout from "./components/AppLayout"; import { DateProvider } from "./components/Date"; import { LocaleProvider } from "./components/Locale"; import { MessageManager } from "./components/messages"; @@ -29,6 +31,7 @@ import ThemeProvider from "./components/Theme"; import { WindowTitle } from "./components/WindowTitle"; import { API_URI, APP_MOUNT_URI } from "./config"; import ConfigurationSection, { createConfigurationMenu } from "./configuration"; +import AppStateProvider from "./containers/AppState"; import { CustomerSection } from "./customers"; import DiscountSection from "./discounts"; import HomePage from "./home"; @@ -119,11 +122,11 @@ const App: React.FC = () => { - + - + @@ -135,6 +138,7 @@ const App: React.FC = () => { const Routes: React.FC = () => { const intl = useIntl(); + const [, dispatchAppState] = useAppState(); return ( <> @@ -148,109 +152,124 @@ const Routes: React.FC = () => { user }) => isAuthenticated && !tokenAuthLoading && !tokenVerifyLoading ? ( - - - - - - - - - - - - - - - - - - - - - {createConfigurationMenu(intl).filter(menu => - menu.menuItems.map(item => hasPermission(item.permission, user)) - ).length > 0 && ( - - )} - - + + + dispatchAppState({ + payload: { + error: "unhandled" + }, + type: "displayError" + }) + } + > + + + + + + + + + + + + + + + + + + + + + {createConfigurationMenu(intl).filter(menu => + menu.menuItems.map(item => + hasPermission(item.permission, user) + ) + ).length > 0 && ( + + )} + + + + ) : hasToken && tokenVerifyLoading ? ( ) : ( diff --git a/src/queries.tsx b/src/queries.tsx index bd71dc233..1428f5db4 100644 --- a/src/queries.tsx +++ b/src/queries.tsx @@ -5,9 +5,7 @@ import React from "react"; import { Query, QueryResult } from "react-apollo"; import { useIntl } from "react-intl"; -import AppProgress from "./components/AppProgress"; -import NotFoundPage from "./components/NotFoundPage"; -import useNavigator from "./hooks/useNavigator"; +import useAppState from "./hooks/useAppState"; import useNotifier from "./hooks/useNotifier"; import { commonMessages } from "./intl"; import { maybe, RequireAtLeastOne } from "./misc"; @@ -68,89 +66,108 @@ export function TypedQuery( query: DocumentNode ): React.FC> { return ({ children, displayLoader, skip, variables, require }) => { - const navigate = useNavigator(); const pushMessage = useNotifier(); + const [, dispatchAppState] = useAppState(); const intl = useIntl(); return ( - - {({ setProgressState }) => ( - - {(queryData: QueryResult) => { - if (queryData.error) { - if ( - !queryData.error.graphQLErrors.every( - err => - maybe(() => err.extensions.exception.code) === - "PermissionDenied" - ) - ) { - pushMessage({ - text: intl.formatMessage(commonMessages.somethingWentWrong) - }); + + {(queryData: QueryResult) => { + if (queryData.error) { + if ( + !queryData.error.graphQLErrors.every( + err => + maybe(() => err.extensions.exception.code) === + "PermissionDenied" + ) + ) { + pushMessage({ + 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 } + }); - 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" + }); + } - let childrenOrNotFound = children({ + if (displayLoader) { + return ( + + dispatchAppState({ + payload: { + value: false + }, + type: "displayLoader" + }) + } + onLoading={() => + dispatchAppState({ + payload: { + value: true + }, + type: "displayLoader" + }) + } + > + {children({ + ...queryData, + loadMore + })} + + ); + } + + return ( + <> + {children({ ...queryData, loadMore - }); - if ( - !queryData.loading && - require && - queryData.data && - !require.reduce( - (acc, key) => acc && queryData.data[key] !== null, - true - ) - ) { - childrenOrNotFound = ( - navigate("/")} /> - ); - } - - if (displayLoader) { - return ( - setProgressState(false)} - onLoading={() => setProgressState(true)} - > - {childrenOrNotFound} - - ); - } - - return <>{childrenOrNotFound}; - }} - - )} - + })} + + ); + }} + ); }; } From 8a6cd1ef1f663823a237e93b8b18115b37fa5e3d Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 14 Nov 2019 15:37:32 +0100 Subject: [PATCH 4/7] Create makeQuery hook --- src/categories/queries.ts | 7 +- src/categories/views/CategoryDetails.tsx | 452 +++++++++--------- .../views/CategoryList/CategoryList.tsx | 271 ++++++----- src/hooks/makeQuery.ts | 115 +++++ 4 files changed, 470 insertions(+), 375 deletions(-) create mode 100644 src/hooks/makeQuery.ts 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; From afec6332723584614ffbd82eaa75d2cbb08a5c75 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 14 Nov 2019 15:40:02 +0100 Subject: [PATCH 5/7] Fix shadowing labels --- src/products/utils/data.ts | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index 735e165bd..4c3a2eec6 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -174,19 +174,23 @@ export function getProductUpdatePageFormData( publicationDate: maybe(() => product.publicationDate, ""), seoDescription: maybe(() => product.seoDescription, ""), seoTitle: maybe(() => product.seoTitle, ""), - sku: maybe(() => - product.productType.hasVariants - ? undefined - : variants && variants[0] - ? variants[0].sku - : undefined + sku: maybe( + () => + product.productType.hasVariants + ? undefined + : variants && variants[0] + ? variants[0].sku + : undefined, + "" ), - stockQuantity: maybe(() => - product.productType.hasVariants - ? undefined - : variants && variants[0] - ? variants[0].quantity - : undefined + stockQuantity: maybe( + () => + product.productType.hasVariants + ? undefined + : variants && variants[0] + ? variants[0].quantity + : undefined, + 0 ) }; } From b18382c978c87833a8c35dd7024fc173c4311616 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 14 Nov 2019 15:55:42 +0100 Subject: [PATCH 6/7] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f06456061..202ced51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable, unreleased changes to this project will be documented in this file. - Fix tax settings updating - #243 by @dominik-zeglen - Add secret fields in plugin configuration - #246 by @dominik-zeglen - Fix subcategories pagination - #249 by @dominik-zeglen +- Use Apollo Hooks - #254 by @dominik-zeglen ## 2.0.0 From 55389464533df4f0ac3691ce98068dbafde9bc8c Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Thu, 14 Nov 2019 16:05:25 +0100 Subject: [PATCH 7/7] Update snapshots --- src/storybook/__snapshots__/Stories.test.ts.snap | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 9b17eea50..a3993eaca 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -97221,6 +97221,7 @@ Ctrl + K" disabled="" name="sku" type="text" + value="" />