Explicitely return not found page

This commit is contained in:
dominik-zeglen 2020-02-20 15:18:22 +01:00
parent 8ed641eee8
commit fa8231992f
22 changed files with 337 additions and 268 deletions

View file

@ -23,6 +23,7 @@ module.exports = api => {
const plugins = [ const plugins = [
"@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-class-properties",
[ [
"@babel/plugin-proposal-decorators", "@babel/plugin-proposal-decorators",
@ -49,6 +50,6 @@ module.exports = api => {
return { return {
ignore, ignore,
plugins, plugins,
presets, presets
}; };
}; };

35
package-lock.json generated
View file

@ -849,6 +849,24 @@
"@babel/plugin-syntax-optional-catch-binding": "^7.2.0" "@babel/plugin-syntax-optional-catch-binding": "^7.2.0"
} }
}, },
"@babel/plugin-proposal-optional-chaining": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.8.3.tgz",
"integrity": "sha512-QIoIR9abkVn+seDE3OjA08jWcs3eZ9+wJCKSRgo3WdEU2csFYgdScb+8qHB3+WXsGJD55u+5hWCISI7ejXS+kg==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.8.3",
"@babel/plugin-syntax-optional-chaining": "^7.8.0"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz",
"integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==",
"dev": true
}
}
},
"@babel/plugin-proposal-unicode-property-regex": { "@babel/plugin-proposal-unicode-property-regex": {
"version": "7.7.0", "version": "7.7.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.7.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.7.0.tgz",
@ -940,6 +958,23 @@
"@babel/helper-plugin-utils": "^7.0.0" "@babel/helper-plugin-utils": "^7.0.0"
} }
}, },
"@babel/plugin-syntax-optional-chaining": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
"integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.8.0"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz",
"integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==",
"dev": true
}
}
},
"@babel/plugin-syntax-top-level-await": { "@babel/plugin-syntax-top-level-await": {
"version": "7.7.0", "version": "7.7.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.7.0.tgz", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.7.0.tgz",

View file

@ -75,6 +75,7 @@
"@babel/plugin-proposal-decorators": "^7.4.4", "@babel/plugin-proposal-decorators": "^7.4.4",
"@babel/plugin-proposal-numeric-separator": "^7.2.0", "@babel/plugin-proposal-numeric-separator": "^7.2.0",
"@babel/plugin-proposal-object-rest-spread": "^7.5.4", "@babel/plugin-proposal-object-rest-spread": "^7.5.4",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/preset-env": "^7.5.4", "@babel/preset-env": "^7.5.4",
"@babel/preset-react": "^7.7.4", "@babel/preset-react": "^7.7.4",
"@babel/preset-typescript": "^7.7.4", "@babel/preset-typescript": "^7.7.4",

View file

@ -14,6 +14,7 @@ import usePaginator, {
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { PAGINATE_BY } from "../../config"; import { PAGINATE_BY } from "../../config";
import { maybe } from "../../misc"; import { maybe } from "../../misc";
import { TypedProductBulkDeleteMutation } from "../../products/mutations"; import { TypedProductBulkDeleteMutation } from "../../products/mutations";
@ -67,10 +68,15 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
const paginationState = createPaginationState(PAGINATE_BY, params); const paginationState = createPaginationState(PAGINATE_BY, params);
const { data, loading, refetch } = useCategoryDetailsQuery({ const { data, loading, refetch } = useCategoryDetailsQuery({
displayLoader: true, displayLoader: true,
require: ["category"],
variables: { ...paginationState, id } variables: { ...paginationState, id }
}); });
const category = data?.category;
if (category === null) {
return <NotFoundPage onBack={() => navigate(categoryListUrl())} />;
}
const handleCategoryDelete = (data: CategoryDelete) => { const handleCategoryDelete = (data: CategoryDelete) => {
if (data.categoryDelete.errors.length === 0) { if (data.categoryDelete.errors.length === 0) {
notify({ notify({

View file

@ -68,7 +68,6 @@ export const CategoryList: React.FC<CategoryListProps> = ({ params }) => {
); );
const { data, loading, refetch } = useRootCategoriesQuery({ const { data, loading, refetch } = useRootCategoriesQuery({
displayLoader: true, displayLoader: true,
require: ["categories"],
variables: queryVariables variables: queryVariables
}); });

View file

@ -16,6 +16,7 @@ import usePaginator, {
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import useProductSearch from "@saleor/searches/useProductSearch"; import useProductSearch from "@saleor/searches/useProductSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { getMutationState, maybe } from "../../misc"; import { getMutationState, maybe } from "../../misc";
import { productUrl } from "../../products/urls"; import { productUrl } from "../../products/urls";
import { CollectionInput } from "../../types/globalTypes"; import { CollectionInput } from "../../types/globalTypes";
@ -61,14 +62,19 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
>(navigate, params => collectionUrl(id, params), params); >(navigate, params => collectionUrl(id, params), params);
const paginationState = createPaginationState(PAGINATE_BY, params); const paginationState = createPaginationState(PAGINATE_BY, params);
const handleBack = () => navigate(collectionListUrl());
return ( return (
<TypedCollectionDetailsQuery <TypedCollectionDetailsQuery
displayLoader displayLoader
variables={{ id, ...paginationState }} variables={{ id, ...paginationState }}
require={["collection"]}
> >
{({ data, loading }) => { {({ data, loading }) => {
const collection = data?.collection;
if (collection === null) {
return <NotFoundPage onBack={handleBack} />;
}
const handleCollectionUpdate = (data: CollectionUpdate) => { const handleCollectionUpdate = (data: CollectionUpdate) => {
if (data.collectionUpdate.errors.length === 0) { if (data.collectionUpdate.errors.length === 0) {
notify({ notify({
@ -196,7 +202,7 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
<WindowTitle title={maybe(() => data.collection.name)} /> <WindowTitle title={maybe(() => data.collection.name)} />
<CollectionDetailsPage <CollectionDetailsPage
onAdd={() => openModal("assign")} onAdd={() => openModal("assign")}
onBack={() => navigate(collectionListUrl())} onBack={handleBack}
disabled={loading} disabled={loading}
collection={maybe(() => data.collection)} collection={maybe(() => data.collection)}
isFeatured={maybe( isFeatured={maybe(

View file

@ -29,7 +29,6 @@ import { maybe } from "@saleor/misc";
import { staffMemberDetailsUrl } from "@saleor/staff/urls"; import { staffMemberDetailsUrl } from "@saleor/staff/urls";
import Container from "../Container"; import Container from "../Container";
import ErrorPage from "../ErrorPage"; import ErrorPage from "../ErrorPage";
import NotFoundPage from "../NotFoundPage";
import AppActionContext from "./AppActionContext"; import AppActionContext from "./AppActionContext";
import AppHeaderContext from "./AppHeaderContext"; import AppHeaderContext from "./AppHeaderContext";
import { appLoaderHeight, drawerWidth, drawerWidthExpanded } from "./consts"; import { appLoaderHeight, drawerWidth, drawerWidthExpanded } from "./consts";
@ -507,15 +506,11 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
</Container> </Container>
</div> </div>
<main className={classes.view}> <main className={classes.view}>
{appState.error ? ( {appState.error
appState.error === "not-found" ? ( ? appState.error === "unhandled" && (
<NotFoundPage onBack={handleErrorBack} /> <ErrorPage onBack={handleErrorBack} />
) : ( )
<ErrorPage onBack={handleErrorBack} /> : children}
)
) : (
children
)}
</main> </main>
</div> </div>
<div className={classes.appAction} ref={appActionAnchor} /> <div className={classes.appAction} ref={appActionAnchor} />

View file

@ -21,7 +21,7 @@ const AppStateProvider: React.FC = ({ children }) => {
if (!!state.error) { if (!!state.error) {
dispatch({ dispatch({
payload: { payload: {
error: null error: undefined
}, },
type: "displayError" type: "displayError"
}); });

View file

@ -1,4 +1,4 @@
export type AppError = "unhandled" | "not-found"; export type AppError = "unhandled";
interface IAppState { interface IAppState {
error: AppError | null; error: AppError | null;

View file

@ -7,6 +7,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { maybe } from "../../misc"; import { maybe } from "../../misc";
import { orderListUrl, orderUrl } from "../../orders/urls"; import { orderListUrl, orderUrl } from "../../orders/urls";
import CustomerDetailsPage from "../components/CustomerDetailsPage/CustomerDetailsPage"; import CustomerDetailsPage from "../components/CustomerDetailsPage/CustomerDetailsPage";
@ -54,6 +55,9 @@ export const CustomerDetailsView: React.FC<CustomerDetailsViewProps> = ({
navigate(customerListUrl()); navigate(customerListUrl());
} }
}; };
const handleBack = () => navigate(customerListUrl());
return ( return (
<TypedRemoveCustomerMutation <TypedRemoveCustomerMutation
variables={{ id }} variables={{ id }}
@ -62,91 +66,97 @@ export const CustomerDetailsView: React.FC<CustomerDetailsViewProps> = ({
{(removeCustomer, removeCustomerOpts) => ( {(removeCustomer, removeCustomerOpts) => (
<TypedUpdateCustomerMutation onCompleted={handleCustomerUpdateSuccess}> <TypedUpdateCustomerMutation onCompleted={handleCustomerUpdateSuccess}>
{(updateCustomer, updateCustomerOpts) => ( {(updateCustomer, updateCustomerOpts) => (
<TypedCustomerDetailsQuery <TypedCustomerDetailsQuery displayLoader variables={{ id }}>
displayLoader {customerDetails => {
variables={{ id }} const user = customerDetails.data?.user;
require={["user"]}
> if (user === null) {
{customerDetails => ( return <NotFoundPage onBack={handleBack} />;
<> }
<WindowTitle
title={maybe(() => customerDetails.data.user.email)} return (
/> <>
<CustomerDetailsPage <WindowTitle
customer={maybe(() => customerDetails.data.user)} title={maybe(() => customerDetails.data.user.email)}
disabled={ />
customerDetails.loading || <CustomerDetailsPage
updateCustomerOpts.loading || customer={maybe(() => customerDetails.data.user)}
removeCustomerOpts.loading disabled={
} customerDetails.loading ||
errors={maybe( updateCustomerOpts.loading ||
() => updateCustomerOpts.data.customerUpdate.errors removeCustomerOpts.loading
)} }
saveButtonBar={updateCustomerOpts.status} errors={maybe(
onAddressManageClick={() => () => updateCustomerOpts.data.customerUpdate.errors
navigate(customerAddressesUrl(id)) )}
} saveButtonBar={updateCustomerOpts.status}
onBack={() => navigate(customerListUrl())} onAddressManageClick={() =>
onRowClick={id => navigate(orderUrl(id))} navigate(customerAddressesUrl(id))
onSubmit={formData => }
updateCustomer({ onBack={handleBack}
variables: { onRowClick={id => navigate(orderUrl(id))}
id, onSubmit={formData =>
input: { updateCustomer({
email: formData.email, variables: {
firstName: formData.firstName, id,
isActive: formData.isActive, input: {
lastName: formData.lastName, email: formData.email,
note: formData.note firstName: formData.firstName,
isActive: formData.isActive,
lastName: formData.lastName,
note: formData.note
}
} }
}
})
}
onDelete={() =>
navigate(
customerUrl(id, {
action: "remove"
}) })
) }
} onDelete={() =>
onViewAllOrdersClick={() => navigate(
navigate( customerUrl(id, {
orderListUrl({ action: "remove"
customer: maybe(() => customerDetails.data.user.email) })
}) )
) }
} onViewAllOrdersClick={() =>
/> navigate(
<ActionDialog orderListUrl({
confirmButtonState={removeCustomerOpts.status} customer: maybe(
onClose={() => navigate(customerUrl(id), true)} () => customerDetails.data.user.email
onConfirm={() => removeCustomer()} )
title={intl.formatMessage({ })
defaultMessage: "Delete Customer", )
description: "dialog header" }
})} />
variant="delete" <ActionDialog
open={params.action === "remove"} confirmButtonState={removeCustomerOpts.status}
> onClose={() => navigate(customerUrl(id), true)}
<DialogContentText> onConfirm={() => removeCustomer()}
<FormattedMessage title={intl.formatMessage({
defaultMessage="Are you sure you want to delete {email}?" defaultMessage: "Delete Customer",
description="delete customer, dialog content" description: "dialog header"
values={{ })}
email: ( variant="delete"
<strong> open={params.action === "remove"}
{maybe( >
() => customerDetails.data.user.email, <DialogContentText>
"..." <FormattedMessage
)} defaultMessage="Are you sure you want to delete {email}?"
</strong> description="delete customer, dialog content"
) values={{
}} email: (
/> <strong>
</DialogContentText> {maybe(
</ActionDialog> () => customerDetails.data.user.email,
</> "..."
)} )}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
</>
);
}}
</TypedCustomerDetailsQuery> </TypedCustomerDetailsQuery>
)} )}
</TypedUpdateCustomerMutation> </TypedUpdateCustomerMutation>

View file

@ -18,14 +18,13 @@ export interface LoadMore<TData, TVariables> {
export type UseQueryResult<TData, TVariables> = QueryResult<TData, TVariables> & export type UseQueryResult<TData, TVariables> = QueryResult<TData, TVariables> &
LoadMore<TData, TVariables>; LoadMore<TData, TVariables>;
type UseQueryOpts<TData, TVariables> = Partial<{ type UseQueryOpts<TVariables> = Partial<{
displayLoader: boolean; displayLoader: boolean;
require: Array<keyof TData>;
skip: boolean; skip: boolean;
variables: TVariables; variables: TVariables;
}>; }>;
type UseQueryHook<TData, TVariables> = ( type UseQueryHook<TData, TVariables> = (
opts: UseQueryOpts<TData, TVariables> opts: UseQueryOpts<TVariables>
) => UseQueryResult<TData, TVariables>; ) => UseQueryResult<TData, TVariables>;
function makeQuery<TData, TVariables>( function makeQuery<TData, TVariables>(
@ -33,10 +32,9 @@ function makeQuery<TData, TVariables>(
): UseQueryHook<TData, TVariables> { ): UseQueryHook<TData, TVariables> {
function useQuery({ function useQuery({
displayLoader, displayLoader,
require,
skip, skip,
variables variables
}: UseQueryOpts<TData, TVariables>): UseQueryResult<TData, TVariables> { }: UseQueryOpts<TVariables>): UseQueryResult<TData, TVariables> {
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
const [, dispatchAppState] = useAppState(); const [, dispatchAppState] = useAppState();
@ -89,20 +87,6 @@ function makeQuery<TData, TVariables>(
variables: { ...variables, ...extraVariables } 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 { return {
...queryData, ...queryData,
loadMore loadMore

View file

@ -380,3 +380,7 @@ export function transformFormToAddress<T>(
country: findInEnum(address.country, CountryCode) country: findInEnum(address.country, CountryCode)
}; };
} }
export function getStringOrPlaceholder(s: string | undefined): string {
return s || "...";
}

View file

@ -6,6 +6,7 @@ import useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser"; import useUser from "@saleor/hooks/useUser";
import useCustomerSearch from "@saleor/searches/useCustomerSearch"; import useCustomerSearch from "@saleor/searches/useCustomerSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { customerUrl } from "../../../customers/urls"; import { customerUrl } from "../../../customers/urls";
import { getMutationState, maybe, transformAddressToForm } from "../../../misc"; import { getMutationState, maybe, transformAddressToForm } from "../../../misc";
import { productUrl } from "../../../products/urls"; import { productUrl } from "../../../products/urls";
@ -90,14 +91,17 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
variables: DEFAULT_INITIAL_SEARCH_DATA variables: DEFAULT_INITIAL_SEARCH_DATA
}); });
const handleBack = () => navigate(orderListUrl());
return ( return (
<TypedOrderDetailsQuery <TypedOrderDetailsQuery displayLoader variables={{ id }}>
displayLoader
variables={{ id }}
require={["order"]}
>
{({ data, loading }) => { {({ data, loading }) => {
const order = maybe(() => data.order); const order = data?.order;
if (order === null) {
return <NotFoundPage onBack={handleBack} />;
}
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
OrderUrlDialog, OrderUrlDialog,
OrderUrlQueryParams OrderUrlQueryParams
@ -166,7 +170,7 @@ export const OrderDetails: React.FC<OrderDetailsProps> = ({ id, params }) => {
order: id order: id
}) })
} }
onBack={() => navigate(orderListUrl())} onBack={handleBack}
order={order} order={order}
shippingMethods={maybe( shippingMethods={maybe(
() => data.order.availableShippingMethods, () => data.order.availableShippingMethods,

View file

@ -13,6 +13,7 @@ import { maybe } from "@saleor/misc";
import AssignAttributeDialog from "@saleor/productTypes/components/AssignAttributeDialog"; import AssignAttributeDialog from "@saleor/productTypes/components/AssignAttributeDialog";
import { ReorderEvent } from "@saleor/types"; import { ReorderEvent } from "@saleor/types";
import { AttributeTypeEnum } from "@saleor/types/globalTypes"; import { AttributeTypeEnum } from "@saleor/types/globalTypes";
import NotFoundPage from "@saleor/components/NotFoundPage";
import ProductTypeAttributeUnassignDialog from "../../components/ProductTypeAttributeUnassignDialog"; import ProductTypeAttributeUnassignDialog from "../../components/ProductTypeAttributeUnassignDialog";
import ProductTypeBulkAttributeUnassignDialog from "../../components/ProductTypeBulkAttributeUnassignDialog"; import ProductTypeBulkAttributeUnassignDialog from "../../components/ProductTypeBulkAttributeUnassignDialog";
import ProductTypeDeleteDialog from "../../components/ProductTypeDeleteDialog"; import ProductTypeDeleteDialog from "../../components/ProductTypeDeleteDialog";
@ -54,15 +55,19 @@ export const ProductTypeUpdate: React.FC<ProductTypeUpdateProps> = ({
} }
}); });
const handleBack = () => navigate(productTypeListUrl());
return ( return (
<ProductTypeUpdateErrors> <ProductTypeUpdateErrors>
{({ errors, set: setErrors }) => ( {({ errors, set: setErrors }) => (
<TypedProductTypeDetailsQuery <TypedProductTypeDetailsQuery displayLoader variables={{ id }}>
displayLoader
variables={{ id }}
require={["productType"]}
>
{({ data, loading: dataLoading }) => { {({ data, loading: dataLoading }) => {
const productType = data?.productType;
if (productType === null) {
return <NotFoundPage onBack={handleBack} />;
}
const closeModal = () => navigate(productTypeUrl(id), true); const closeModal = () => navigate(productTypeUrl(id), true);
const handleAttributeAssignSuccess = (data: AssignAttribute) => { const handleAttributeAssignSuccess = (data: AssignAttribute) => {
@ -242,7 +247,7 @@ export const ProductTypeUpdate: React.FC<ProductTypeUpdateProps> = ({
}) })
) )
} }
onBack={() => navigate(productTypeListUrl())} onBack={handleBack}
onDelete={() => onDelete={() =>
navigate( navigate(
productTypeUrl(id, { productTypeUrl(id, {

View file

@ -5,6 +5,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { maybe } from "../../misc"; import { maybe } from "../../misc";
import ProductImagePage from "../components/ProductImagePage"; import ProductImagePage from "../components/ProductImagePage";
import { import {
@ -16,7 +17,8 @@ import { ProductImageUpdate } from "../types/ProductImageUpdate";
import { import {
productImageUrl, productImageUrl,
ProductImageUrlQueryParams, ProductImageUrlQueryParams,
productUrl productUrl,
productListUrl
} from "../urls"; } from "../urls";
interface ProductImageProps { interface ProductImageProps {
@ -47,70 +49,77 @@ export const ProductImage: React.FC<ProductImageProps> = ({
imageId, imageId,
productId productId
}} }}
require={["product"]}
> >
{({ data, loading }) => ( {({ data, loading }) => {
<TypedProductImageUpdateMutation onCompleted={handleUpdateSuccess}> const product = data?.product;
{(updateImage, updateResult) => (
<TypedProductImageDeleteMutation onCompleted={handleBack}>
{(deleteImage, deleteResult) => {
const handleDelete = () =>
deleteImage({ variables: { id: imageId } });
const handleImageClick = (id: string) => () =>
navigate(productImageUrl(productId, id));
const handleUpdate = (formData: { description: string }) => {
updateImage({
variables: {
alt: formData.description,
id: imageId
}
});
};
const image = data && data.product && data.product.mainImage;
return ( if (product === null) {
<> return <NotFoundPage onBack={() => navigate(productListUrl())} />;
<ProductImagePage }
disabled={loading}
product={maybe(() => data.product.name)} return (
image={image || null} <TypedProductImageUpdateMutation onCompleted={handleUpdateSuccess}>
images={maybe(() => data.product.images)} {(updateImage, updateResult) => (
onBack={handleBack} <TypedProductImageDeleteMutation onCompleted={handleBack}>
onDelete={() => {(deleteImage, deleteResult) => {
navigate( const handleDelete = () =>
productImageUrl(productId, imageId, { deleteImage({ variables: { id: imageId } });
action: "remove" const handleImageClick = (id: string) => () =>
}) navigate(productImageUrl(productId, id));
) const handleUpdate = (formData: { description: string }) => {
updateImage({
variables: {
alt: formData.description,
id: imageId
} }
onRowClick={handleImageClick} });
onSubmit={handleUpdate} };
saveButtonBarState={updateResult.status} const image = data && data.product && data.product.mainImage;
/>
<ActionDialog return (
onClose={() => <>
navigate(productImageUrl(productId, imageId), true) <ProductImagePage
} disabled={loading}
onConfirm={handleDelete} product={maybe(() => data.product.name)}
open={params.action === "remove"} image={image || null}
title={intl.formatMessage({ images={maybe(() => data.product.images)}
defaultMessage: "Delete Image", onBack={handleBack}
description: "dialog header" onDelete={() =>
})} navigate(
variant="delete" productImageUrl(productId, imageId, {
confirmButtonState={deleteResult.status} action: "remove"
> })
<DialogContentText> )
<FormattedMessage defaultMessage="Are you sure you want to delete this image?" /> }
</DialogContentText> onRowClick={handleImageClick}
</ActionDialog> onSubmit={handleUpdate}
</> saveButtonBarState={updateResult.status}
); />
}} <ActionDialog
</TypedProductImageDeleteMutation> onClose={() =>
)} navigate(productImageUrl(productId, imageId), true)
</TypedProductImageUpdateMutation> }
)} onConfirm={handleDelete}
open={params.action === "remove"}
title={intl.formatMessage({
defaultMessage: "Delete Image",
description: "dialog header"
})}
variant="delete"
confirmButtonState={deleteResult.status}
>
<DialogContentText>
<FormattedMessage defaultMessage="Are you sure you want to delete this image?" />
</DialogContentText>
</ActionDialog>
</>
);
}}
</TypedProductImageDeleteMutation>
)}
</TypedProductImageUpdateMutation>
);
}}
</TypedProductImageQuery> </TypedProductImageQuery>
); );
}; };

View file

@ -18,6 +18,7 @@ import { ProductVariantBulkCreate } from "@saleor/products/types/ProductVariantB
import useCategorySearch from "@saleor/searches/useCategorySearch"; import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch"; import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { getMutationState, maybe } from "../../../misc"; import { getMutationState, maybe } from "../../../misc";
import ProductUpdatePage from "../../components/ProductUpdatePage"; import ProductUpdatePage from "../../components/ProductUpdatePage";
import ProductUpdateOperations from "../../containers/ProductUpdateOperations"; import ProductUpdateOperations from "../../containers/ProductUpdateOperations";
@ -76,13 +77,17 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
ProductUrlQueryParams ProductUrlQueryParams
>(navigate, params => productUrl(id, params), params); >(navigate, params => productUrl(id, params), params);
const handleBack = () => navigate(productListUrl());
return ( return (
<TypedProductDetailsQuery <TypedProductDetailsQuery displayLoader variables={{ id }}>
displayLoader
require={["product"]}
variables={{ id }}
>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
const product = data?.product;
if (product === null) {
return <NotFoundPage onBack={handleBack} />;
}
const handleDelete = () => { const handleDelete = () => {
notify({ notify({
text: intl.formatMessage({ text: intl.formatMessage({
@ -142,7 +147,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
} }
}; };
const product = data ? data.product : undefined;
return ( return (
<ProductUpdateOperations <ProductUpdateOperations
product={product} product={product}
@ -233,9 +237,7 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
placeholderImage={placeholderImg} placeholderImage={placeholderImg}
product={product} product={product}
variants={maybe(() => product.variants)} variants={maybe(() => product.variants)}
onBack={() => { onBack={handleBack}
navigate(productListUrl());
}}
onDelete={() => openModal("remove")} onDelete={() => openModal("remove")}
onProductShow={() => { onProductShow={() => {
if (product) { if (product) {

View file

@ -6,6 +6,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { decimal, maybe } from "../../misc"; import { decimal, maybe } from "../../misc";
import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog"; import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog";
import ProductVariantPage, { import ProductVariantPage, {
@ -13,7 +14,10 @@ import ProductVariantPage, {
} from "../components/ProductVariantPage"; } from "../components/ProductVariantPage";
import ProductVariantOperations from "../containers/ProductVariantOperations"; import ProductVariantOperations from "../containers/ProductVariantOperations";
import { TypedProductVariantQuery } from "../queries"; import { TypedProductVariantQuery } from "../queries";
import { VariantUpdate } from "../types/VariantUpdate"; import {
VariantUpdate,
VariantUpdate_productVariantUpdate_productErrors
} from "../types/VariantUpdate";
import { import {
productUrl, productUrl,
productVariantAddUrl, productVariantAddUrl,
@ -35,20 +39,24 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
const [errors, setErrors] = useState([]); const [errors, setErrors] = useState<
VariantUpdate_productVariantUpdate_productErrors[]
>([]);
useEffect(() => { useEffect(() => {
setErrors([]); setErrors([]);
}, [variantId]); }, [variantId]);
const handleBack = () => navigate(productUrl(productId));
return ( return (
<TypedProductVariantQuery <TypedProductVariantQuery displayLoader variables={{ id: variantId }}>
displayLoader
variables={{ id: variantId }}
require={["productVariant"]}
>
{({ data, loading }) => { {({ data, loading }) => {
const variant = data ? data.productVariant : undefined; const variant = data?.productVariant;
const handleBack = () => navigate(productUrl(productId));
if (variant === null) {
return <NotFoundPage onBack={handleBack} />;
}
const handleDelete = () => { const handleDelete = () => {
notify({ notify({
text: intl.formatMessage({ text: intl.formatMessage({

View file

@ -5,6 +5,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { decimal, maybe } from "../../misc"; import { decimal, maybe } from "../../misc";
import ProductVariantCreatePage, { import ProductVariantCreatePage, {
ProductVariantCreatePageSubmitData ProductVariantCreatePageSubmitData
@ -12,7 +13,7 @@ import ProductVariantCreatePage, {
import { TypedVariantCreateMutation } from "../mutations"; import { TypedVariantCreateMutation } from "../mutations";
import { TypedProductVariantCreateQuery } from "../queries"; import { TypedProductVariantCreateQuery } from "../queries";
import { VariantCreate } from "../types/VariantCreate"; import { VariantCreate } from "../types/VariantCreate";
import { productUrl, productVariantEditUrl } from "../urls"; import { productUrl, productVariantEditUrl, productListUrl } from "../urls";
interface ProductUpdateProps { interface ProductUpdateProps {
productId: string; productId: string;
@ -25,12 +26,14 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({ productId }) => {
const intl = useIntl(); const intl = useIntl();
return ( return (
<TypedProductVariantCreateQuery <TypedProductVariantCreateQuery displayLoader variables={{ id: productId }}>
displayLoader
variables={{ id: productId }}
require={["product"]}
>
{({ data, loading: productLoading }) => { {({ data, loading: productLoading }) => {
const product = data?.product;
if (product === null) {
return <NotFoundPage onBack={() => navigate(productListUrl())} />;
}
const handleCreateSuccess = (data: VariantCreate) => { const handleCreateSuccess = (data: VariantCreate) => {
if (data.productVariantCreate.productErrors.length === 0) { if (data.productVariantCreate.productErrors.length === 0) {
notify({ notify({

View file

@ -28,7 +28,6 @@ export interface TypedQueryInnerProps<TData, TVariables> {
displayLoader?: boolean; displayLoader?: boolean;
skip?: boolean; skip?: boolean;
variables?: TVariables; variables?: TVariables;
require?: Array<keyof TData>;
} }
interface QueryProgressProps { interface QueryProgressProps {
@ -65,7 +64,7 @@ class QueryProgress extends React.Component<QueryProgressProps, {}> {
export function TypedQuery<TData, TVariables>( export function TypedQuery<TData, TVariables>(
query: DocumentNode query: DocumentNode
): React.FC<TypedQueryInnerProps<TData, TVariables>> { ): React.FC<TypedQueryInnerProps<TData, TVariables>> {
return ({ children, displayLoader, skip, variables, require }) => { return ({ children, displayLoader, skip, variables }) => {
const pushMessage = useNotifier(); const pushMessage = useNotifier();
const [, dispatchAppState] = useAppState(); const [, dispatchAppState] = useAppState();
const intl = useIntl(); const intl = useIntl();
@ -112,23 +111,6 @@ export function TypedQuery<TData, TVariables>(
variables: { ...variables, ...extraVariables } 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"
});
}
if (displayLoader) { if (displayLoader) {
return ( return (
<QueryProgress <QueryProgress

View file

@ -22,6 +22,7 @@ import { ServiceTokenCreate } from "@saleor/services/types/ServiceTokenCreate";
import { ServiceTokenDelete } from "@saleor/services/types/ServiceTokenDelete"; import { ServiceTokenDelete } from "@saleor/services/types/ServiceTokenDelete";
import { ServiceUpdate } from "@saleor/services/types/ServiceUpdate"; import { ServiceUpdate } from "@saleor/services/types/ServiceUpdate";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import NotFoundPage from "@saleor/components/NotFoundPage";
import ServiceDetailsPage, { import ServiceDetailsPage, {
ServiceDetailsPageFormData ServiceDetailsPageFormData
} from "../../components/ServiceDetailsPage"; } from "../../components/ServiceDetailsPage";
@ -77,12 +78,14 @@ export const ServiceDetails: React.FC<OrderListProps> = ({
const handleBack = () => navigate(serviceListUrl()); const handleBack = () => navigate(serviceListUrl());
return ( return (
<ServiceDetailsQuery <ServiceDetailsQuery displayLoader variables={{ id }}>
displayLoader
variables={{ id }}
require={["serviceAccount"]}
>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
const service = data?.serviceAccount;
if (service === null) {
return <NotFoundPage onBack={handleBack} />;
}
const onTokenCreate = (data: ServiceTokenCreate) => { const onTokenCreate = (data: ServiceTokenCreate) => {
if (maybe(() => data.serviceAccountTokenCreate.errors.length === 0)) { if (maybe(() => data.serviceAccountTokenCreate.errors.length === 0)) {
refetch(); refetch();
@ -151,9 +154,7 @@ export const ServiceDetails: React.FC<OrderListProps> = ({
return ( return (
<> <>
<WindowTitle <WindowTitle title={service?.name || "..."} />
title={maybe(() => data.serviceAccount.name)}
/>
<ServiceDetailsPage <ServiceDetailsPage
apiUri={API_URI} apiUri={API_URI}
disabled={loading} disabled={loading}

View file

@ -9,6 +9,7 @@ import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import useUser from "@saleor/hooks/useUser"; import useUser from "@saleor/hooks/useUser";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { maybe } from "../../misc"; import { maybe } from "../../misc";
import StaffDetailsPage from "../components/StaffDetailsPage/StaffDetailsPage"; import StaffDetailsPage from "../components/StaffDetailsPage/StaffDetailsPage";
import { import {
@ -63,13 +64,17 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
onCompleted: handleChangePassword onCompleted: handleChangePassword
}); });
const handleBack = () => navigate(staffListUrl());
return ( return (
<TypedStaffMemberDetailsQuery <TypedStaffMemberDetailsQuery displayLoader variables={{ id }}>
displayLoader
variables={{ id }}
require={["user"]}
>
{({ data, loading }) => { {({ data, loading }) => {
const staffMember = data?.user;
if (staffMember === null) {
return <NotFoundPage onBack={handleBack} />;
}
const handleStaffMemberUpdate = (data: StaffMemberUpdate) => { const handleStaffMemberUpdate = (data: StaffMemberUpdate) => {
if (!maybe(() => data.staffUpdate.errors.length !== 0)) { if (!maybe(() => data.staffUpdate.errors.length !== 0)) {
notify({ notify({
@ -100,6 +105,7 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
navigate(staffMemberDetailsUrl(id)); navigate(staffMemberDetailsUrl(id));
} }
}; };
return ( return (
<TypedStaffMemberUpdateMutation onCompleted={handleStaffMemberUpdate}> <TypedStaffMemberUpdateMutation onCompleted={handleStaffMemberUpdate}>
{(updateStaffMember, updateResult) => ( {(updateStaffMember, updateResult) => (
@ -124,7 +130,7 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
return ( return (
<> <>
<WindowTitle <WindowTitle
title={maybe(() => data.user.email)} title={staffMember?.email || "..."}
/> />
<StaffDetailsPage <StaffDetailsPage
canEditAvatar={isUserSameAsViewer} canEditAvatar={isUserSameAsViewer}
@ -132,7 +138,7 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
canEditStatus={!isUserSameAsViewer} canEditStatus={!isUserSameAsViewer}
canRemove={!isUserSameAsViewer} canRemove={!isUserSameAsViewer}
disabled={loading} disabled={loading}
onBack={() => navigate(staffListUrl())} onBack={handleBack}
onChangePassword={() => onChangePassword={() =>
navigate( navigate(
staffMemberDetailsUrl(id, { staffMemberDetailsUrl(id, {
@ -176,7 +182,7 @@ export const StaffDetails: React.FC<OrderListProps> = ({ id, params }) => {
) )
} }
permissions={maybe(() => shop.permissions)} permissions={maybe(() => shop.permissions)}
staffMember={maybe(() => data.user)} staffMember={staffMember}
saveButtonBarState={updateResult.status} saveButtonBarState={updateResult.status}
/> />
<ActionDialog <ActionDialog

View file

@ -12,7 +12,8 @@ import WebhookDeleteDialog from "@saleor/webhooks/components/WebhookDeleteDialog
import { WebhookDelete } from "@saleor/webhooks/types/WebhookDelete"; import { WebhookDelete } from "@saleor/webhooks/types/WebhookDelete";
import { WebhookUpdate } from "@saleor/webhooks/types/WebhookUpdate"; import { WebhookUpdate } from "@saleor/webhooks/types/WebhookUpdate";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import { maybe } from "../../misc"; import NotFoundPage from "@saleor/components/NotFoundPage";
import { maybe, getStringOrPlaceholder } from "../../misc";
import WebhooksDetailsPage from "../components/WebhooksDetailsPage"; import WebhooksDetailsPage from "../components/WebhooksDetailsPage";
import { TypedWebhookDelete, TypedWebhookUpdate } from "../mutations"; import { TypedWebhookDelete, TypedWebhookUpdate } from "../mutations";
import { TypedWebhooksDetailsQuery } from "../queries"; import { TypedWebhooksDetailsQuery } from "../queries";
@ -48,7 +49,7 @@ export const WebhooksDetails: React.FC<WebhooksDetailsProps> = ({
>(navigate, params => webhookUrl(id, params), params); >(navigate, params => webhookUrl(id, params), params);
const onWebhookDelete = (data: WebhookDelete) => { const onWebhookDelete = (data: WebhookDelete) => {
if (data.webhookDelete.errors.length === 0) { if (data.webhookDelete?.errors.length === 0) {
notify({ notify({
text: intl.formatMessage(commonMessages.savedChanges) text: intl.formatMessage(commonMessages.savedChanges)
}); });
@ -57,14 +58,19 @@ export const WebhooksDetails: React.FC<WebhooksDetailsProps> = ({
}; };
const onWebhookUpdate = (data: WebhookUpdate) => { const onWebhookUpdate = (data: WebhookUpdate) => {
if (data.webhookUpdate.webhookErrors.length === 0) { const errors = data.webhookUpdate?.webhookErrors;
const webhook = data.webhookUpdate?.webhook;
if (errors.length === 0 && webhook) {
notify({ notify({
text: intl.formatMessage(commonMessages.savedChanges) text: intl.formatMessage(commonMessages.savedChanges)
}); });
navigate(webhookUrl(data.webhookUpdate.webhook.id)); navigate(webhookUrl(webhook.id));
} }
}; };
const handleOnBack = () => navigate(webhookListUrl());
return ( return (
<TypedWebhookUpdate onCompleted={onWebhookUpdate}> <TypedWebhookUpdate onCompleted={onWebhookUpdate}>
{(webhookUpdate, webhookUpdateOpts) => ( {(webhookUpdate, webhookUpdateOpts) => (
@ -79,28 +85,33 @@ export const WebhooksDetails: React.FC<WebhooksDetailsProps> = ({
} }
}); });
const formErrors = maybe( const webhook = webhookDetails?.data?.webhook;
() => webhookUpdateOpts.data.webhookUpdate.webhookErrors, const formErrors =
[] webhookUpdateOpts.data?.webhookUpdate.webhookErrors || [];
);
if (webhook === null) {
return <NotFoundPage onBack={handleOnBack} />;
}
return ( return (
<> <>
<WindowTitle <WindowTitle
title={maybe(() => webhookDetails.data.webhook.name)} title={getStringOrPlaceholder(
webhookDetails?.data?.webhook?.name
)}
/> />
<WebhooksDetailsPage <WebhooksDetailsPage
disabled={webhookDetails.loading} disabled={webhookDetails.loading}
errors={formErrors} errors={formErrors}
saveButtonBarState={webhookUpdateOpts.status} saveButtonBarState={webhookUpdateOpts.status}
webhook={maybe(() => webhookDetails.data.webhook)} webhook={webhook}
fetchServiceAccounts={searchServiceAccount} fetchServiceAccounts={searchServiceAccount}
services={maybe(() => services={maybe(() =>
searchServiceAccountOpt.data.search.edges.map( searchServiceAccountOpt.data.search.edges.map(
edge => edge.node edge => edge.node
) )
)} )}
onBack={() => navigate(webhookListUrl())} onBack={handleOnBack}
onDelete={() => openModal("remove")} onDelete={() => openModal("remove")}
onSubmit={data => { onSubmit={data => {
webhookUpdate({ webhookUpdate({
@ -122,10 +133,7 @@ export const WebhooksDetails: React.FC<WebhooksDetailsProps> = ({
/> />
<WebhookDeleteDialog <WebhookDeleteDialog
confirmButtonState={webhookDeleteOpts.status} confirmButtonState={webhookDeleteOpts.status}
name={maybe( name={getStringOrPlaceholder(webhook?.name)}
() => webhookDetails.data.webhook.name,
"..."
)}
onClose={closeModal} onClose={closeModal}
onConfirm={handleRemoveConfirm} onConfirm={handleRemoveConfirm}
open={params.action === "remove"} open={params.action === "remove"}