Merge pull request #254 from mirumee/ref/use-apollo-hooks
Use Apollo Hooks
This commit is contained in:
commit
bb8af8938f
23 changed files with 1206 additions and 977 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -43,8 +43,7 @@ const useStyles = makeStyles(theme => ({
|
|||
root: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
width: "100vw"
|
||||
height: "calc(100vh - 180px)"
|
||||
}
|
||||
}));
|
||||
|
||||
|
|
40
src/containers/AppState/AppState.tsx
Normal file
40
src/containers/AppState/AppState.tsx
Normal 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;
|
2
src/containers/AppState/index.ts
Normal file
2
src/containers/AppState/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./AppState";
|
||||
export * from "./AppState";
|
42
src/containers/AppState/reducer.ts
Normal file
42
src/containers/AppState/reducer.ts
Normal 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;
|
13
src/containers/AppState/state.ts
Normal file
13
src/containers/AppState/state.ts
Normal 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
65
src/hooks/makeMutation.ts
Normal 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
115
src/hooks/makeQuery.ts
Normal 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
11
src/hooks/useAppState.ts
Normal 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;
|
231
src/index.tsx
231
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 = () => {
|
|||
<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 />
|
||||
) : (
|
||||
|
|
|
@ -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
|
||||
)
|
||||
};
|
||||
}
|
||||
|
|
169
src/queries.tsx
169
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 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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue