Create makeQuery hook

This commit is contained in:
dominik-zeglen 2019-11-14 15:37:32 +01:00
parent b12f3c35fa
commit 8a6cd1ef1f
4 changed files with 470 additions and 375 deletions

View file

@ -1,6 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import { pageInfoFragment, TypedQuery } from "../queries"; import makeQuery from "@saleor/hooks/makeQuery";
import { pageInfoFragment } from "../queries";
import { import {
CategoryDetails, CategoryDetails,
CategoryDetailsVariables CategoryDetailsVariables
@ -65,7 +66,7 @@ export const rootCategories = gql`
} }
} }
`; `;
export const TypedRootCategoriesQuery = TypedQuery<RootCategories, {}>( export const useRootCategoriesQuery = makeQuery<RootCategories, {}>(
rootCategories rootCategories
); );
@ -119,7 +120,7 @@ export const categoryDetails = gql`
} }
} }
`; `;
export const TypedCategoryDetailsQuery = TypedQuery< export const useCategoryDetailsQuery = makeQuery<
CategoryDetails, CategoryDetails,
CategoryDetailsVariables CategoryDetailsVariables
>(categoryDetails); >(categoryDetails);

View file

@ -28,7 +28,7 @@ import {
useCategoryDeleteMutation, useCategoryDeleteMutation,
useCategoryUpdateMutation useCategoryUpdateMutation
} from "../mutations"; } from "../mutations";
import { TypedCategoryDetailsQuery } from "../queries"; import { useCategoryDetailsQuery } from "../queries";
import { CategoryBulkDelete } from "../types/CategoryBulkDelete"; import { CategoryBulkDelete } from "../types/CategoryBulkDelete";
import { CategoryDelete } from "../types/CategoryDelete"; import { CategoryDelete } from "../types/CategoryDelete";
import { CategoryUpdate } from "../types/CategoryUpdate"; import { CategoryUpdate } from "../types/CategoryUpdate";
@ -63,6 +63,13 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
); );
const intl = useIntl(); 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) => { const handleCategoryDelete = (data: CategoryDelete) => {
if (data.categoryDelete.errors.length === 0) { if (data.categoryDelete.errors.length === 0) {
notify({ notify({
@ -140,7 +147,6 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
}) })
); );
const paginationState = createPaginationState(PAGINATE_BY, params);
const formTransitionState = getMutationState( const formTransitionState = getMutationState(
updateResult.called, updateResult.called,
updateResult.loading, updateResult.loading,
@ -152,245 +158,221 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
maybe(() => deleteResult.data.categoryDelete.errors) 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 ( return (
<TypedCategoryDetailsQuery <>
displayLoader <WindowTitle title={maybe(() => data.category.name)} />
variables={{ ...paginationState, id }} <TypedProductBulkDeleteMutation onCompleted={handleBulkProductDelete}>
require={["category"]} {(productBulkDelete, productBulkDeleteOpts) => {
> const categoryBulkDeleteMutationState = getMutationState(
{({ data, loading, refetch }) => { categoryBulkDeleteOpts.called,
const handleBulkProductDelete = (data: productBulkDelete) => { categoryBulkDeleteOpts.loading,
if (data.productBulkDelete.errors.length === 0) { maybe(() => categoryBulkDeleteOpts.data.categoryBulkDelete.errors)
closeModal(); );
notify({ const productBulkDeleteMutationState = getMutationState(
text: intl.formatMessage(commonMessages.savedChanges) productBulkDeleteOpts.called,
}); productBulkDeleteOpts.loading,
refetch(); maybe(() => productBulkDeleteOpts.data.productBulkDelete.errors)
reset(); );
}
};
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( return (
params.activeTab === CategoryPageTab.categories <>
? maybe(() => data.category.children.pageInfo) <CategoryUpdatePage
: maybe(() => data.category.products.pageInfo), changeTab={changeTab}
paginationState, currentTab={params.activeTab}
params category={maybe(() => data.category)}
); disabled={loading}
errors={maybe(() => updateResult.data.categoryUpdate.errors)}
return ( onAddCategory={() => navigate(categoryAddUrl(id))}
<> onAddProduct={() => navigate(productAddUrl)}
<WindowTitle title={maybe(() => data.category.name)} /> onBack={() =>
<TypedProductBulkDeleteMutation navigate(
onCompleted={handleBulkProductDelete} maybe(
> () => categoryUrl(data.category.parent.id),
{(productBulkDelete, productBulkDeleteOpts) => { categoryListUrl()
const categoryBulkDeleteMutationState = getMutationState( )
categoryBulkDeleteOpts.called,
categoryBulkDeleteOpts.loading,
maybe(
() => categoryBulkDeleteOpts.data.categoryBulkDelete.errors
) )
); }
const productBulkDeleteMutationState = getMutationState( onCategoryClick={id => () => navigate(categoryUrl(id))}
productBulkDeleteOpts.called, onDelete={() => openModal("delete")}
productBulkDeleteOpts.loading, onImageDelete={() =>
maybe( updateCategory({
() => productBulkDeleteOpts.data.productBulkDelete.errors variables: {
) id,
); input: {
backgroundImage: null
return (
<>
<CategoryUpdatePage
changeTab={changeTab}
currentTab={params.activeTab}
category={maybe(() => 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({ onImageUpload={file =>
variables: { updateCategory({
id, variables: {
input: { id,
backgroundImage: null input: {
} backgroundImage: file
}
})
} }
onImageUpload={file => }
updateCategory({ })
variables: { }
id, onNextPage={loadNextPage}
input: { onPreviousPage={loadPreviousPage}
backgroundImage: file 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))} products={maybe(() =>
onSubmit={formData => data.category.products.edges.map(edge => edge.node)
updateCategory({ )}
variables: { saveButtonBarState={formTransitionState}
id, subcategories={maybe(() =>
input: { data.category.children.edges.map(edge => edge.node)
backgroundImageAlt: formData.backgroundImageAlt, )}
descriptionJson: JSON.stringify( subcategoryListToolbar={
formData.description <IconButton
), color="primary"
name: formData.name, onClick={() => openModal("delete-categories", listElements)}
seo: { >
description: formData.seoDescription, <DeleteIcon />
title: formData.seoTitle </IconButton>
} }
} productListToolbar={
} <IconButton
}) color="primary"
} onClick={() => openModal("delete-products", listElements)}
products={maybe(() => >
data.category.products.edges.map(edge => edge.node) <DeleteIcon />
)} </IconButton>
saveButtonBarState={formTransitionState} }
subcategories={maybe(() => isChecked={isSelected}
data.category.children.edges.map(edge => edge.node) selected={listElements.length}
)} toggle={toggle}
subcategoryListToolbar={ toggleAll={toggleAll}
<IconButton />
color="primary" <ActionDialog
onClick={() => confirmButtonState={removeDialogTransitionState}
openModal("delete-categories", listElements) onClose={closeModal}
} onConfirm={() => deleteCategory({ variables: { id } })}
> open={params.action === "delete"}
<DeleteIcon /> title={intl.formatMessage({
</IconButton> defaultMessage: "Delete category",
} description: "dialog title"
productListToolbar={ })}
<IconButton variant="delete"
color="primary" >
onClick={() => <DialogContentText>
openModal("delete-products", listElements) <FormattedMessage
} defaultMessage="Are you sure you want to delete {categoryName}?"
> values={{
<DeleteIcon /> categoryName: (
</IconButton> <strong>
} {maybe(() => data.category.name, "...")}
isChecked={isSelected} </strong>
selected={listElements.length} )
toggle={toggle} }}
toggleAll={toggleAll} />
/> </DialogContentText>
<ActionDialog <DialogContentText>
confirmButtonState={removeDialogTransitionState} <FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
onClose={closeModal} </DialogContentText>
onConfirm={() => deleteCategory({ variables: { id } })} </ActionDialog>
open={params.action === "delete"} <ActionDialog
title={intl.formatMessage({ open={
defaultMessage: "Delete category", params.action === "delete-categories" &&
description: "dialog title" maybe(() => params.ids.length > 0)
})} }
variant="delete" confirmButtonState={categoryBulkDeleteMutationState}
> onClose={closeModal}
<DialogContentText> onConfirm={() =>
<FormattedMessage categoryBulkDelete({
defaultMessage="Are you sure you want to delete {categoryName}?" variables: { ids: params.ids }
values={{ }).then(() => refetch())
categoryName: ( }
<strong> title={intl.formatMessage({
{maybe(() => data.category.name, "...")} defaultMessage: "Delete categories",
</strong> description: "dialog title"
) })}
}} variant="delete"
/> >
</DialogContentText> <DialogContentText>
<DialogContentText> <FormattedMessage
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." /> defaultMessage="Are you sure you want to delete {counter,plural,one{this category} other{{displayQuantity} categories}}?"
</DialogContentText> values={{
</ActionDialog> counter: maybe(() => params.ids.length),
<ActionDialog displayQuantity: (
open={ <strong>{maybe(() => params.ids.length)}</strong>
params.action === "delete-categories" && )
maybe(() => params.ids.length > 0) }}
} />
confirmButtonState={categoryBulkDeleteMutationState} </DialogContentText>
onClose={closeModal} <DialogContentText>
onConfirm={() => <FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
categoryBulkDelete({ </DialogContentText>
variables: { ids: params.ids } </ActionDialog>
}).then(() => refetch()) <ActionDialog
} open={params.action === "delete-products"}
title={intl.formatMessage({ confirmButtonState={productBulkDeleteMutationState}
defaultMessage: "Delete categories", onClose={closeModal}
description: "dialog title" onConfirm={() =>
})} productBulkDelete({
variant="delete" variables: { ids: params.ids }
> }).then(() => refetch())
<DialogContentText> }
<FormattedMessage title={intl.formatMessage({
defaultMessage="Are you sure you want to delete {counter,plural,one{this category} other{{displayQuantity} categories}}?" defaultMessage: "Delete products",
values={{ description: "dialog title"
counter: maybe(() => params.ids.length), })}
displayQuantity: ( variant="delete"
<strong>{maybe(() => params.ids.length)}</strong> >
) <DialogContentText>
}} <FormattedMessage
/> defaultMessage="Are you sure you want to delete {counter,plural,one{this product} other{{displayQuantity} products}}?"
</DialogContentText> values={{
<DialogContentText> counter: maybe(() => params.ids.length),
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." /> displayQuantity: (
</DialogContentText> <strong>{maybe(() => params.ids.length)}</strong>
</ActionDialog> )
<ActionDialog }}
open={params.action === "delete-products"} />
confirmButtonState={productBulkDeleteMutationState} </DialogContentText>
onClose={closeModal} </ActionDialog>
onConfirm={() => </>
productBulkDelete({ );
variables: { ids: params.ids } }}
}).then(() => refetch()) </TypedProductBulkDeleteMutation>
} </>
title={intl.formatMessage({
defaultMessage: "Delete products",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {counter,plural,one{this product} other{{displayQuantity} products}}?"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
</>
);
}}
</TypedProductBulkDeleteMutation>
</>
);
}}
</TypedCategoryDetailsQuery>
); );
}; };
export default CategoryDetails; export default CategoryDetails;

View file

@ -19,7 +19,7 @@ import { getMutationState, maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types"; import { ListViews } from "@saleor/types";
import { CategoryListPage } from "../../components/CategoryListPage/CategoryListPage"; import { CategoryListPage } from "../../components/CategoryListPage/CategoryListPage";
import { useCategoryBulkDeleteMutation } from "../../mutations"; import { useCategoryBulkDeleteMutation } from "../../mutations";
import { TypedRootCategoriesQuery } from "../../queries"; import { useRootCategoriesQuery } from "../../queries";
import { CategoryBulkDelete } from "../../types/CategoryBulkDelete"; import { CategoryBulkDelete } from "../../types/CategoryBulkDelete";
import { import {
categoryAddUrl, categoryAddUrl,
@ -53,6 +53,20 @@ export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
); );
const intl = useIntl(); 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 tabs = getFilterTabs();
const currentTab = const currentTab =
@ -113,146 +127,129 @@ export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
handleTabChange(tabs.length + 1); handleTabChange(tabs.length + 1);
}; };
const paginationState = createPaginationState(settings.rowNumber, params); const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
const queryVariables = React.useMemo( maybe(() => data.categories.pageInfo),
() => ({ paginationState,
...paginationState, params
filter: getFilterVariables(params) );
}),
[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 ( return (
<TypedRootCategoriesQuery displayLoader variables={queryVariables}> <>
{({ data, loading, refetch }) => { <CategoryListPage
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( categories={maybe(
maybe(() => data.categories.pageInfo), () => data.categories.edges.map(edge => edge.node),
paginationState, []
params )}
); currentTab={currentTab}
initialSearch={params.query || ""}
const handleCategoryBulkDelete = (data: CategoryBulkDelete) => { onSearchChange={query => changeFilterField({ query })}
if (data.categoryBulkDelete.errors.length === 0) { onAll={() => navigate(categoryListUrl())}
navigate(categoryListUrl(), true); onTabChange={handleTabChange}
refetch(); onTabDelete={() => openModal("delete-search")}
reset(); onTabSave={() => openModal("save-search")}
} tabs={tabs.map(tab => tab.name)}
}; settings={settings}
onAdd={() => navigate(categoryAddUrl())}
const [ onRowClick={id => () => navigate(categoryUrl(id))}
categoryBulkDelete, disabled={loading}
categoryBulkDeleteOpts onNextPage={loadNextPage}
] = useCategoryBulkDeleteMutation({ onPreviousPage={loadPreviousPage}
onCompleted: handleCategoryBulkDelete onUpdateListSettings={updateListSettings}
}); pageInfo={pageInfo}
isChecked={isSelected}
const bulkDeleteState = getMutationState( selected={listElements.length}
categoryBulkDeleteOpts.called, toggle={toggle}
categoryBulkDeleteOpts.loading, toggleAll={toggleAll}
maybe(() => categoryBulkDeleteOpts.data.categoryBulkDelete.errors) toolbar={
); <IconButton
color="primary"
return ( onClick={() =>
<> navigate(
<CategoryListPage categoryListUrl({
categories={maybe( ...params,
() => data.categories.edges.map(edge => edge.node), action: "delete",
[] ids: listElements
)}
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={
<IconButton
color="primary"
onClick={() =>
navigate(
categoryListUrl({
...params,
action: "delete",
ids: listElements
})
)
}
>
<DeleteIcon />
</IconButton>
}
/>
<ActionDialog
confirmButtonState={bulkDeleteState}
onClose={() =>
navigate(
categoryListUrl({
...params,
action: undefined,
ids: undefined
})
)
}
onConfirm={() =>
categoryBulkDelete({
variables: {
ids: params.ids
}
}) })
} )
open={params.action === "delete"} }
title={intl.formatMessage({ >
defaultMessage: "Delete categories", <DeleteIcon />
description: "dialog title" </IconButton>
})} }
variant="delete" />
> <ActionDialog
<DialogContentText> confirmButtonState={bulkDeleteState}
<FormattedMessage onClose={() =>
defaultMessage="Are you sure you want to delete {counter,plural,one{this category} other{{displayQuantity} categories}}?" navigate(
values={{ categoryListUrl({
counter: maybe(() => params.ids.length), ...params,
displayQuantity: ( action: undefined,
<strong>{maybe(() => params.ids.length)}</strong> ids: undefined
) })
}} )
/> }
</DialogContentText> onConfirm={() =>
<DialogContentText> categoryBulkDelete({
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." /> variables: {
</DialogContentText> ids: params.ids
</ActionDialog> }
<SaveFilterTabDialog })
open={params.action === "save-search"} }
confirmButtonState="default" open={params.action === "delete"}
onClose={closeModal} title={intl.formatMessage({
onSubmit={handleTabSave} defaultMessage: "Delete categories",
/> description: "dialog title"
<DeleteFilterTabDialog })}
open={params.action === "delete-search"} variant="delete"
confirmButtonState="default" >
onClose={closeModal} <DialogContentText>
onSubmit={handleTabDelete} <FormattedMessage
tabName={maybe(() => tabs[currentTab - 1].name, "...")} defaultMessage="Are you sure you want to delete {counter,plural,one{this category} other{{displayQuantity} categories}}?"
/> values={{
</> counter: maybe(() => params.ids.length),
); displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
}} }}
</TypedRootCategoriesQuery> />
</DialogContentText>
<DialogContentText>
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
</DialogContentText>
</ActionDialog>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabDelete}
tabName={maybe(() => tabs[currentTab - 1].name, "...")}
/>
</>
); );
}; };
export default CategoryList; export default CategoryList;

115
src/hooks/makeQuery.ts Normal file
View file

@ -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<TData, TVariables> {
loadMore: (
mergeFunc: (prev: TData, next: TData) => TData,
extraVariables: Partial<TVariables>
) => Promise<ApolloQueryResult<TData>>;
}
type UseQuery<TData, TVariables> = QueryResult<TData, TVariables> &
LoadMore<TData, TVariables>;
type UseQueryOpts<TData, TVariables> = Partial<{
displayLoader: boolean;
require: Array<keyof TData>;
skip: boolean;
variables: TVariables;
}>;
type UseQueryHook<TData, TVariables> = (
opts: UseQueryOpts<TData, TVariables>
) => UseQuery<TData, TVariables>;
function makeQuery<TData, TVariables>(
query: DocumentNode
): UseQueryHook<TData, TVariables> {
function useQuery<TData, TVariables>({
displayLoader,
require,
skip,
variables
}: UseQueryOpts<TData, TVariables>): UseQuery<TData, TVariables> {
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<TVariables>
) =>
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;