Merge pull request #254 from mirumee/ref/use-apollo-hooks

Use Apollo Hooks
This commit is contained in:
Marcin Gębala 2019-11-15 14:13:27 +01:00 committed by GitHub
commit bb8af8938f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1206 additions and 977 deletions

View file

@ -9,6 +9,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add secret fields in plugin configuration - #246 by @dominik-zeglen
- Fix subcategories pagination - #249 by @dominik-zeglen
- Update customer's details page design - #248 by @dominik-zeglen
- Use Apollo Hooks - #254 by @dominik-zeglen
## 2.0.0

View file

@ -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",

View file

@ -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<SectionRouteProps> = ({
permissions,
...props
}) => {
const navigate = useNavigator();
const { user } = useUser();
const hasPermissions =
@ -26,18 +21,7 @@ export const SectionRoute: React.FC<SectionRouteProps> = ({
permissions
.map(permission => hasPermission(permission, user))
.reduce((prev, curr) => prev && curr);
return hasPermissions ? (
<AppLayout>
<ErrorBoundary
FallbackComponent={() => <ErrorPage onBack={() => navigate("/")} />}
key={permissions ? permissions.join(":") : "home"}
>
<Route {...props} />
</ErrorBoundary>
</AppLayout>
) : (
<NotFound />
);
return hasPermissions ? <Route {...props} /> : <NotFound />;
};
SectionRoute.displayName = "Route";
export default SectionRoute;

View file

@ -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);

View file

@ -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<RootCategories, {}>(
export const useRootCategoriesQuery = makeQuery<RootCategories, {}>(
rootCategories
);
@ -119,7 +120,7 @@ export const categoryDetails = gql`
}
}
`;
export const TypedCategoryDetailsQuery = TypedQuery<
export const useCategoryDetailsQuery = makeQuery<
CategoryDetails,
CategoryDetailsVariables
>(categoryDetails);

View file

@ -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<CategoryCreateViewProps> = ({
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 (
<TypedCategoryCreateMutation onCompleted={handleSuccess}>
{(createCategory, createCategoryResult) => {
const errors = maybe(
() => createCategoryResult.data.categoryCreate.errors,
[]
);
const formTransitionState = getMutationState(
createCategoryResult.called,
createCategoryResult.loading,
errors
);
return (
<>
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Create category",
description: "window title"
})}
/>
<CategoryCreatePage
saveButtonBarState={formTransitionState}
errors={errors}
disabled={createCategoryResult.loading}
onBack={() =>
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
}
})
}
/>
</>
);
}}
</TypedCategoryCreateMutation>
<>
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Create category",
description: "window title"
})}
/>
<CategoryCreatePage
saveButtonBarState={formTransitionState}
errors={errors}
disabled={createCategoryResult.loading}
onBack={() =>
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;

View file

@ -24,11 +24,11 @@ import {
CategoryUpdatePage
} from "../components/CategoryUpdatePage/CategoryUpdatePage";
import {
TypedCategoryBulkDeleteMutation,
TypedCategoryDeleteMutation,
TypedCategoryUpdateMutation
useCategoryBulkDeleteMutation,
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<CategoryDetailsProps> = ({
);
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({
@ -73,6 +80,11 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
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 +98,27 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
}
};
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 +147,232 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
})
);
const formTransitionState = getMutationState(
updateResult.called,
updateResult.loading,
maybe(() => updateResult.data.categoryUpdate.errors)
);
const removeDialogTransitionState = getMutationState(
deleteResult.called,
deleteResult.loading,
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 (
<TypedCategoryDeleteMutation onCompleted={handleCategoryDelete}>
{(deleteCategory, deleteResult) => (
<TypedCategoryUpdateMutation onCompleted={handleCategoryUpdate}>
{(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 (
<TypedCategoryDetailsQuery
displayLoader
variables={{ ...paginationState, id }}
require={["category"]}
<>
<WindowTitle title={maybe(() => data.category.name)} />
<TypedProductBulkDeleteMutation onCompleted={handleBulkProductDelete}>
{(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 (
<>
<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({
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={
<IconButton
color="primary"
onClick={() => openModal("delete-categories", listElements)}
>
<DeleteIcon />
</IconButton>
}
productListToolbar={
<IconButton
color="primary"
onClick={() => openModal("delete-products", listElements)}
>
<DeleteIcon />
</IconButton>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
/>
<ActionDialog
confirmButtonState={removeDialogTransitionState}
onClose={closeModal}
onConfirm={() => deleteCategory({ variables: { id } })}
open={params.action === "delete"}
title={intl.formatMessage({
defaultMessage: "Delete category",
description: "dialog title"
})}
variant="delete"
>
{({ data, loading, refetch }) => {
const handleBulkCategoryDelete = (
data: CategoryBulkDelete
) => {
if (data.categoryBulkDelete.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
);
return (
<>
<WindowTitle title={maybe(() => data.category.name)} />
<TypedCategoryBulkDeleteMutation
onCompleted={handleBulkCategoryDelete}
>
{(categoryBulkDelete, categoryBulkDeleteOpts) => (
<TypedProductBulkDeleteMutation
onCompleted={handleBulkProductDelete}
>
{(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 (
<>
<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({
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={
<IconButton
color="primary"
onClick={() =>
openModal(
"delete-categories",
listElements
)
}
>
<DeleteIcon />
</IconButton>
}
productListToolbar={
<IconButton
color="primary"
onClick={() =>
openModal(
"delete-products",
listElements
)
}
>
<DeleteIcon />
</IconButton>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
/>
<ActionDialog
confirmButtonState={
removeDialogTransitionState
}
onClose={closeModal}
onConfirm={() =>
deleteCategory({ variables: { id } })
}
open={params.action === "delete"}
title={intl.formatMessage({
defaultMessage: "Delete category",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {categoryName}?"
values={{
categoryName: (
<strong>
{maybe(
() => data.category.name,
"..."
)}
</strong>
)
}}
/>
</DialogContentText>
<DialogContentText>
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
</DialogContentText>
</ActionDialog>
<ActionDialog
open={
params.action === "delete-categories" &&
maybe(() => 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"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {counter,plural,one{this attribute} other{{displayQuantity} categories}}?"
values={{
counter: maybe(
() => params.ids.length
),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
<DialogContentText>
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "delete-products"}
confirmButtonState={
productBulkDeleteMutationState
}
onClose={closeModal}
onConfirm={() =>
productBulkDelete({
variables: { ids: params.ids }
})
}
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 attribute} other{{displayQuantity} products}}?"
values={{
counter: maybe(
() => params.ids.length
),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
</>
);
}}
</TypedProductBulkDeleteMutation>
)}
</TypedCategoryBulkDeleteMutation>
</>
);
}}
</TypedCategoryDetailsQuery>
);
}}
</TypedCategoryUpdateMutation>
)}
</TypedCategoryDeleteMutation>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {categoryName}?"
values={{
categoryName: (
<strong>
{maybe(() => data.category.name, "...")}
</strong>
)
}}
/>
</DialogContentText>
<DialogContentText>
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
</DialogContentText>
</ActionDialog>
<ActionDialog
open={
params.action === "delete-categories" &&
maybe(() => 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"
>
<DialogContentText>
<FormattedMessage
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>
)
}}
/>
</DialogContentText>
<DialogContentText>
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "delete-products"}
confirmButtonState={productBulkDeleteMutationState}
onClose={closeModal}
onConfirm={() =>
productBulkDelete({
variables: { ids: params.ids }
}).then(() => refetch())
}
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>
</>
);
};
export default CategoryDetails;

View file

@ -18,8 +18,8 @@ import usePaginator, {
import { getMutationState, maybe } from "@saleor/misc";
import { ListViews } from "@saleor/types";
import { CategoryListPage } from "../../components/CategoryListPage/CategoryListPage";
import { TypedCategoryBulkDeleteMutation } from "../../mutations";
import { TypedRootCategoriesQuery } from "../../queries";
import { useCategoryBulkDeleteMutation } from "../../mutations";
import { useRootCategoriesQuery } from "../../queries";
import { CategoryBulkDelete } from "../../types/CategoryBulkDelete";
import {
categoryAddUrl,
@ -53,6 +53,20 @@ export const CategoryList: React.FC<CategoryListProps> = ({ 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,148 +127,129 @@ export const CategoryList: React.FC<CategoryListProps> = ({ 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 (
<TypedRootCategoriesQuery displayLoader variables={queryVariables}>
{({ 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();
}
};
return (
<TypedCategoryBulkDeleteMutation
onCompleted={handleCategoryBulkDelete}
<>
<CategoryListPage
categories={maybe(
() => 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={
<IconButton
color="primary"
onClick={() =>
navigate(
categoryListUrl({
...params,
action: "delete",
ids: listElements
})
)
}
>
{(categoryBulkDelete, categoryBulkDeleteOpts) => {
const bulkDeleteState = getMutationState(
categoryBulkDeleteOpts.called,
categoryBulkDeleteOpts.loading,
maybe(
() => categoryBulkDeleteOpts.data.categoryBulkDelete.errors
)
);
return (
<>
<CategoryListPage
categories={maybe(
() => 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={
<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",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
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>
)
}}
/>
</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, "...")}
/>
</>
);
<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",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
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>
}}
</TypedCategoryBulkDeleteMutation>
);
}}
</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;

View file

@ -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<AppLayoutProps & RouteComponentProps<any>, any>(
({ children, location }: AppLayoutProps & RouteComponentProps<any>) => {
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<HTMLDivElement>();
const appHeaderAnchor = React.useRef<HTMLDivElement>();
const anchor = React.useRef<HTMLDivElement>();
const { logout, user } = useUser();
const navigate = useNavigator();
const intl = useIntl();
const AppLayout: React.FC<AppLayoutProps> = ({ 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<HTMLDivElement>();
const appHeaderAnchor = React.useRef<HTMLDivElement>();
const anchor = React.useRef<HTMLDivElement>();
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<any>) => {
event.stopPropagation();
event.preventDefault();
setDrawerState(false);
navigate(url);
};
const handleMenuItemClick = (url: string, event: React.MouseEvent<any>) => {
event.stopPropagation();
event.preventDefault();
setDrawerState(false);
navigate(url);
};
const handleIsMenuSmall = () => {
setMenuSmall(!isMenuSmall);
};
const handleIsMenuSmall = () => {
setMenuSmall(!isMenuSmall);
};
return (
<AppProgressProvider>
{({ isProgress }) => (
<AppHeaderContext.Provider value={appHeaderAnchor}>
<AppActionContext.Provider value={appActionAnchor}>
<div className={classes.root}>
<div className={classes.sideBar}>
<ResponsiveDrawer
onClose={() => setDrawerState(false)}
open={isDrawerOpened}
small={!isMenuSmall}
>
<div
className={classNames(classes.logo, {
[classes.logoSmall]: isMenuSmall,
[classes.logoDark]: isDark
})}
>
<SVG
src={isMenuSmall ? saleorDarkLogoSmall : saleorDarkLogo}
/>
</div>
<Hidden smDown>
<div
className={classNames(classes.isMenuSmall, {
[classes.isMenuSmallHide]: isMenuSmall,
[classes.isMenuSmallDark]: isDark
})}
onClick={handleIsMenuSmall}
>
<SVG src={menuArrowIcon} />
</div>
</Hidden>
<MenuList
className={isMenuSmall ? classes.menuSmall : classes.menu}
menuItems={menuStructure}
isMenuSmall={!isMenuSmall}
location={location.pathname}
user={user}
renderConfigure={renderConfigure}
onMenuItemClick={handleMenuItemClick}
/>
</ResponsiveDrawer>
</div>
<div
className={classNames(classes.content, {
[classes.contentToggle]: isMenuSmall
})}
>
{isProgress ? (
<LinearProgress
className={classes.appLoader}
color="primary"
/>
) : (
<div className={classes.appLoaderPlaceholder} />
)}
<div className={classes.viewContainer}>
<div>
<Container>
<div className={classes.header}>
<div
className={classNames(classes.menuIcon, {
[classes.menuIconOpen]: isDrawerOpened,
[classes.menuIconDark]: isDark
})}
onClick={() => setDrawerState(!isDrawerOpened)}
>
<span />
<span />
<span />
<span />
</div>
<div ref={appHeaderAnchor} />
<div className={classes.spacer} />
<div className={classes.userBar}>
<ThemeSwitch
className={classes.darkThemeSwitch}
checked={isDark}
onClick={toggleTheme}
/>
<div
className={classes.userMenuContainer}
ref={anchor}
>
<Chip
avatar={
user.avatar && (
<Avatar alt="user" src={user.avatar.url} />
)
}
className={classes.userChip}
label={
<>
{user.email}
<ArrowDropdown
className={classNames(classes.arrow, {
[classes.rotate]: isMenuOpened
})}
/>
</>
}
onClick={() => setMenuState(!isMenuOpened)}
/>
<Popper
className={classes.popover}
open={isMenuOpened}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom-end"
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom"
? "right top"
: "right bottom"
}}
>
<Paper>
<ClickAwayListener
onClickAway={() => setMenuState(false)}
mouseEvent="onClick"
>
<Menu>
<MenuItem
className={classes.userMenuItem}
onClick={handleViewerProfile}
>
<FormattedMessage
defaultMessage="Account Settings"
description="button"
/>
</MenuItem>
<MenuItem
className={classes.userMenuItem}
onClick={handleLogout}
>
<FormattedMessage
defaultMessage="Log out"
description="button"
/>
</MenuItem>
</Menu>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
</div>
</Container>
</div>
<main className={classes.view}>{children}</main>
</div>
<div className={classes.appAction} ref={appActionAnchor} />
</div>
const handleErrorBack = () => {
navigate("/");
dispatchAppState({
payload: {
error: null
},
type: "displayError"
});
};
return (
<AppHeaderContext.Provider value={appHeaderAnchor}>
<AppActionContext.Provider value={appActionAnchor}>
<div className={classes.root}>
<div className={classes.sideBar}>
<ResponsiveDrawer
onClose={() => setDrawerState(false)}
open={isDrawerOpened}
small={!isMenuSmall}
>
<div
className={classNames(classes.logo, {
[classes.logoSmall]: isMenuSmall,
[classes.logoDark]: isDark
})}
>
<SVG src={isMenuSmall ? saleorDarkLogoSmall : saleorDarkLogo} />
</div>
</AppActionContext.Provider>
</AppHeaderContext.Provider>
)}
</AppProgressProvider>
);
}
);
<Hidden smDown>
<div
className={classNames(classes.isMenuSmall, {
[classes.isMenuSmallHide]: isMenuSmall,
[classes.isMenuSmallDark]: isDark
})}
onClick={handleIsMenuSmall}
>
<SVG src={menuArrowIcon} />
</div>
</Hidden>
<MenuList
className={isMenuSmall ? classes.menuSmall : classes.menu}
menuItems={menuStructure}
isMenuSmall={!isMenuSmall}
location={location.pathname}
user={user}
renderConfigure={renderConfigure}
onMenuItemClick={handleMenuItemClick}
/>
</ResponsiveDrawer>
</div>
<div
className={classNames(classes.content, {
[classes.contentToggle]: isMenuSmall
})}
>
{appState.loading ? (
<LinearProgress className={classes.appLoader} color="primary" />
) : (
<div className={classes.appLoaderPlaceholder} />
)}
<div className={classes.viewContainer}>
<div>
<Container>
<div className={classes.header}>
<div
className={classNames(classes.menuIcon, {
[classes.menuIconOpen]: isDrawerOpened,
[classes.menuIconDark]: isDark
})}
onClick={() => setDrawerState(!isDrawerOpened)}
>
<span />
<span />
<span />
<span />
</div>
<div ref={appHeaderAnchor} />
<div className={classes.spacer} />
<div className={classes.userBar}>
<ThemeSwitch
className={classes.darkThemeSwitch}
checked={isDark}
onClick={toggleTheme}
/>
<div className={classes.userMenuContainer} ref={anchor}>
<Chip
avatar={
user.avatar && (
<Avatar alt="user" src={user.avatar.url} />
)
}
className={classes.userChip}
label={
<>
{user.email}
<ArrowDropdown
className={classNames(classes.arrow, {
[classes.rotate]: isMenuOpened
})}
/>
</>
}
onClick={() => setMenuState(!isMenuOpened)}
/>
<Popper
className={classes.popover}
open={isMenuOpened}
anchorEl={anchor.current}
transition
disablePortal
placement="bottom-end"
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom"
? "right top"
: "right bottom"
}}
>
<Paper>
<ClickAwayListener
onClickAway={() => setMenuState(false)}
mouseEvent="onClick"
>
<Menu>
<MenuItem
className={classes.userMenuItem}
onClick={handleViewerProfile}
>
<FormattedMessage
defaultMessage="Account Settings"
description="button"
/>
</MenuItem>
<MenuItem
className={classes.userMenuItem}
onClick={handleLogout}
>
<FormattedMessage
defaultMessage="Log out"
description="button"
/>
</MenuItem>
</Menu>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
</div>
</Container>
</div>
<main className={classes.view}>
{appState.error ? (
appState.error === "not-found" ? (
<NotFoundPage onBack={handleErrorBack} />
) : (
<ErrorPage onBack={handleErrorBack} />
)
) : (
children
)}
</main>
</div>
<div className={classes.appAction} ref={appActionAnchor} />
</div>
</div>
</AppActionContext.Provider>
</AppHeaderContext.Provider>
);
};
export default AppLayout;

View file

@ -1,28 +0,0 @@
import React from "react";
interface IAppProgressContext {
isProgress: boolean;
setProgressState: (isOpened: boolean) => void;
}
export const AppProgressContext = React.createContext<IAppProgressContext>(
undefined
);
export const AppProgressProvider: React.FC<{}> = ({ children }) => {
const [isProgress, setProgressState] = React.useState(false);
return (
<AppProgressContext.Provider
value={{
isProgress,
setProgressState
}}
>
{children}
</AppProgressContext.Provider>
);
};
export const AppProgress = AppProgressContext.Consumer;
export default AppProgress;

View file

@ -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

View file

@ -43,8 +43,7 @@ const useStyles = makeStyles(theme => ({
root: {
alignItems: "center",
display: "flex",
height: "100vh",
width: "100vw"
height: "calc(100vh - 180px)"
}
}));

View file

@ -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<AppStateReducerAction>
];
export const AppStateContext = React.createContext<AppStateContextType>([
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 (
<AppStateContext.Provider value={stateAndDispatch}>
{children}
</AppStateContext.Provider>
);
};
export const { Consumer } = AppStateContext;
export default AppStateProvider;

View file

@ -0,0 +1,2 @@
export { default } from "./AppState";
export * from "./AppState";

View file

@ -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;

View file

@ -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;

65
src/hooks/makeMutation.ts Normal file
View file

@ -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<TData, TVariables> = [
MutationFunction<TData, TVariables>,
MutationResult<TData>
];
type UseMutationCbs<TData> = Partial<{
onCompleted: (data: TData) => void;
onError: (error: ApolloError) => void;
}>;
type UseMutationHook<TData, TVariables> = (
cbs: UseMutationCbs<TData>
) => UseMutation<TData, TVariables>;
function makeMutation<TData, TVariables>(
mutation: DocumentNode
): UseMutationHook<TData, TVariables> {
function useMutation<TData, TVariables>({
onCompleted,
onError
}: UseMutationCbs<TData>): UseMutation<TData, TVariables> {
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;

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;

11
src/hooks/useAppState.ts Normal file
View file

@ -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;

View file

@ -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 = () => {
<DateProvider>
<LocaleProvider>
<MessageManager>
<AppProgressProvider>
<AppStateProvider>
<ShopProvider>
<Routes />
</ShopProvider>
</AppProgressProvider>
</AppStateProvider>
</MessageManager>
</LocaleProvider>
</DateProvider>
@ -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 ? (
<Switch>
<SectionRoute exact path="/" component={HomePage} />
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/categories"
component={CategorySection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/collections"
component={CollectionSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_USERS]}
path="/customers"
component={CustomerSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_DISCOUNTS]}
path="/discounts"
component={DiscountSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PAGES]}
path="/pages"
component={PageSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PLUGINS]}
path="/plugins"
component={PluginsSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_ORDERS]}
path="/orders"
component={OrdersSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/products"
component={ProductSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/product-types"
component={ProductTypesSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_STAFF]}
path="/staff"
component={StaffSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_SETTINGS]}
path="/site-settings"
component={SiteSettingsSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_SETTINGS]}
path="/taxes"
component={TaxesSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_SHIPPING]}
path="/shipping"
component={ShippingSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_TRANSLATIONS]}
path="/translations"
component={TranslationsSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_WEBHOOKS]}
path="/webhooks"
component={WebhooksSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_MENUS]}
path={navigationSection}
component={NavigationSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path={attributeSection}
component={AttributeSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_SERVICE_ACCOUNTS]}
path={serviceSection}
component={ServiceSection}
/>
{createConfigurationMenu(intl).filter(menu =>
menu.menuItems.map(item => hasPermission(item.permission, user))
).length > 0 && (
<SectionRoute
exact
path="/configuration"
component={ConfigurationSection}
/>
)}
<Route component={NotFound} />
</Switch>
<AppLayout>
<ErrorBoundary
onError={() =>
dispatchAppState({
payload: {
error: "unhandled"
},
type: "displayError"
})
}
>
<Switch>
<SectionRoute exact path="/" component={HomePage} />
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/categories"
component={CategorySection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/collections"
component={CollectionSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_USERS]}
path="/customers"
component={CustomerSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_DISCOUNTS]}
path="/discounts"
component={DiscountSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PAGES]}
path="/pages"
component={PageSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PLUGINS]}
path="/plugins"
component={PluginsSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_ORDERS]}
path="/orders"
component={OrdersSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/products"
component={ProductSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path="/product-types"
component={ProductTypesSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_STAFF]}
path="/staff"
component={StaffSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_SETTINGS]}
path="/site-settings"
component={SiteSettingsSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_SETTINGS]}
path="/taxes"
component={TaxesSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_SHIPPING]}
path="/shipping"
component={ShippingSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_TRANSLATIONS]}
path="/translations"
component={TranslationsSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_WEBHOOKS]}
path="/webhooks"
component={WebhooksSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_MENUS]}
path={navigationSection}
component={NavigationSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_PRODUCTS]}
path={attributeSection}
component={AttributeSection}
/>
<SectionRoute
permissions={[PermissionEnum.MANAGE_SERVICE_ACCOUNTS]}
path={serviceSection}
component={ServiceSection}
/>
{createConfigurationMenu(intl).filter(menu =>
menu.menuItems.map(item =>
hasPermission(item.permission, user)
)
).length > 0 && (
<SectionRoute
exact
path="/configuration"
component={ConfigurationSection}
/>
)}
<Route component={NotFound} />
</Switch>
</ErrorBoundary>
</AppLayout>
) : hasToken && tokenVerifyLoading ? (
<LoginLoading />
) : (

View file

@ -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
)
};
}

View file

@ -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 ErrorPage from "./components/ErrorPage/ErrorPage";
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,87 +66,108 @@ export function TypedQuery<TData, TVariables>(
query: DocumentNode
): React.FC<TypedQueryInnerProps<TData, TVariables>> {
return ({ children, displayLoader, skip, variables, require }) => {
const navigate = useNavigator();
const pushMessage = useNotifier();
const [, dispatchAppState] = useAppState();
const intl = useIntl();
return (
<AppProgress>
{({ setProgressState }) => (
<Query
fetchPolicy="cache-and-network"
query={query}
variables={variables}
skip={skip}
context={{ useBatching: true }}
errorPolicy="all"
>
{(queryData: QueryResult<TData, TVariables>) => {
if (queryData.error) {
if (
!queryData.error.graphQLErrors.every(
err =>
maybe(() => err.extensions.exception.code) ===
"PermissionDenied"
)
) {
pushMessage({
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
<Query
fetchPolicy="cache-and-network"
query={query}
variables={variables}
skip={skip}
context={{ useBatching: true }}
errorPolicy="all"
>
{(queryData: QueryResult<TData, TVariables>) => {
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<TVariables>
) =>
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<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"
});
}
let childrenOrNotFound = children({
if (displayLoader) {
return (
<QueryProgress
loading={queryData.loading}
onCompleted={() =>
dispatchAppState({
payload: {
value: false
},
type: "displayLoader"
})
}
onLoading={() =>
dispatchAppState({
payload: {
value: true
},
type: "displayLoader"
})
}
>
{children({
...queryData,
loadMore
})}
</QueryProgress>
);
}
return (
<>
{children({
...queryData,
loadMore
});
if (
!queryData.loading &&
require &&
queryData.data &&
!require.reduce(
(acc, key) => acc && queryData.data[key] !== null,
true
)
) {
childrenOrNotFound = <ErrorPage onBack={() => navigate("/")} />;
}
if (displayLoader) {
return (
<QueryProgress
loading={queryData.loading}
onCompleted={() => setProgressState(false)}
onLoading={() => setProgressState(true)}
>
{childrenOrNotFound}
</QueryProgress>
);
}
return <>{childrenOrNotFound}</>;
}}
</Query>
)}
</AppProgress>
})}
</>
);
}}
</Query>
);
};
}

View file

@ -97522,6 +97522,7 @@ Ctrl + K"
disabled=""
name="sku"
type="text"
value=""
/>
<fieldset
aria-hidden="true"
@ -97543,8 +97544,8 @@ Ctrl + K"
class="MuiFormControl-root-id MuiTextField-root-id"
>
<label
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id"
data-shrink="false"
class="MuiFormLabel-root-id MuiInputLabel-root-id MuiInputLabel-formControl-id MuiInputLabel-animated-id MuiInputLabel-shrink-id MuiInputLabel-outlined-id MuiFormLabel-disabled-id MuiInputLabel-disabled-id MuiFormLabel-filled-id"
data-shrink="true"
>
Inventory
</label>
@ -97557,6 +97558,7 @@ Ctrl + K"
disabled=""
name="stockQuantity"
type="number"
value="0"
/>
<fieldset
aria-hidden="true"
@ -97565,7 +97567,7 @@ Ctrl + K"
>
<legend
class="PrivateNotchedOutline-legend-id"
style="width:0.01px"
style="width:0"
>
<span>