Merge pull request #262 from mirumee/ref/hook-searches

Use searches as hooks instead of components
This commit is contained in:
Marcin Gębala 2019-11-21 16:10:22 +01:00 committed by GitHub
commit 29bd1a43a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1812 additions and 1990 deletions

View file

@ -12,6 +12,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Use Apollo Hooks - #254 by @dominik-zeglen - Use Apollo Hooks - #254 by @dominik-zeglen
- Fix disappearing products description - #259 by @dominik-zeglen - Fix disappearing products description - #259 by @dominik-zeglen
- Improve mobile appearance - #240 by @benekex2 and @dominik-zeglen - Improve mobile appearance - #240 by @benekex2 and @dominik-zeglen
- Use searches as hooks instead of components - #262 by @dominik-zeglen
## 2.0.0 ## 2.0.0

View file

@ -6,6 +6,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import AssignProductDialog from "@saleor/components/AssignProductDialog"; import AssignProductDialog from "@saleor/components/AssignProductDialog";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA, PAGINATE_BY } from "@saleor/config";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
@ -13,8 +14,7 @@ import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import { DEFAULT_INITIAL_SEARCH_DATA, PAGINATE_BY } from "../../config"; import useProductSearch from "@saleor/searches/useProductSearch";
import SearchProducts from "../../containers/SearchProducts";
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";
@ -50,6 +50,9 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
); );
const paginate = usePaginator(); const paginate = usePaginator();
const intl = useIntl(); const intl = useIntl();
const { search, result } = useProductSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const closeModal = () => const closeModal = () =>
navigate( navigate(
@ -284,29 +287,25 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
toggle={toggle} toggle={toggle}
toggleAll={toggleAll} toggleAll={toggleAll}
/> />
<SearchProducts variables={DEFAULT_INITIAL_SEARCH_DATA}> <AssignProductDialog
{({ search, result }) => ( confirmButtonState={assignTransitionState}
<AssignProductDialog open={params.action === "assign"}
confirmButtonState={assignTransitionState} onFetch={search}
open={params.action === "assign"} loading={result.loading}
onFetch={search} onClose={closeModal}
loading={result.loading} onSubmit={products =>
onClose={closeModal} assignProduct.mutate({
onSubmit={products => ...paginationState,
assignProduct.mutate({ collectionId: id,
...paginationState, productIds: products.map(product => product.id)
collectionId: id, })
productIds: products.map(product => product.id) }
}) products={maybe(() =>
} result.data.search.edges
products={maybe(() => .map(edge => edge.node)
result.data.search.edges .filter(suggestedProduct => suggestedProduct.id)
.map(edge => edge.node)
.filter(suggestedProduct => suggestedProduct.id)
)}
/>
)} )}
</SearchProducts> />
<ActionDialog <ActionDialog
confirmButtonState={removeTransitionState} confirmButtonState={removeTransitionState}
onClose={closeModal} onClose={closeModal}

View file

@ -19,7 +19,7 @@ import FormSpacer from "@saleor/components/FormSpacer";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import useSearchQuery from "@saleor/hooks/useSearchQuery"; import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { SearchCategories_search_edges_node } from "../../containers/SearchCategories/types/SearchCategories"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
export interface FormData { export interface FormData {

View file

@ -15,7 +15,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import ResponsiveTable from "@saleor/components/ResponsiveTable"; import ResponsiveTable from "@saleor/components/ResponsiveTable";
import useSearchQuery from "@saleor/hooks/useSearchQuery"; import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { SearchCollections_search_edges_node } from "../../containers/SearchCollections/types/SearchCollections"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
import ConfirmButton, { import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState

View file

@ -21,7 +21,7 @@ import TableCellAvatar from "@saleor/components/TableCellAvatar";
import useSearchQuery from "@saleor/hooks/useSearchQuery"; import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { SearchProducts_search_edges_node } from "../../containers/SearchProducts/types/SearchProducts"; import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts";
import Checkbox from "../Checkbox"; import Checkbox from "../Checkbox";
export interface FormData { export interface FormData {

View file

@ -1,10 +1,10 @@
import { SearchQueryVariables } from "./containers/BaseSearch"; import { SearchVariables } from "./hooks/makeSearch";
import { ListSettings, ListViews } from "./types"; import { ListSettings, ListViews } from "./types";
export const APP_MOUNT_URI = process.env.APP_MOUNT_URI || "/"; export const APP_MOUNT_URI = process.env.APP_MOUNT_URI || "/";
export const API_URI = process.env.API_URI || "/graphql/"; export const API_URI = process.env.API_URI || "/graphql/";
export const DEFAULT_INITIAL_SEARCH_DATA: SearchQueryVariables = { export const DEFAULT_INITIAL_SEARCH_DATA: SearchVariables = {
after: null, after: null,
first: 20, first: 20,
query: "" query: ""

View file

@ -1,77 +0,0 @@
import { DocumentNode } from "graphql";
import React from "react";
import Debounce from "../components/Debounce";
import { TypedQuery, TypedQueryResult } from "../queries";
export interface SearchQueryVariables {
after?: string;
first: number;
query: string;
}
interface BaseSearchProps<
TQuery,
TQueryVariables extends SearchQueryVariables
> {
children: (props: {
loadMore: () => void;
search: (query: string) => void;
result: TypedQueryResult<TQuery, TQueryVariables>;
}) => React.ReactElement<any>;
variables: TQueryVariables;
}
function BaseSearch<TQuery, TQueryVariables extends SearchQueryVariables>(
query: DocumentNode,
loadMoreFn: (result: TypedQueryResult<TQuery, TQueryVariables>) => void
) {
const Query = TypedQuery<TQuery, TQueryVariables>(query);
class BaseSearchComponent extends React.Component<
BaseSearchProps<TQuery, TQueryVariables>,
SearchQueryVariables
> {
state: SearchQueryVariables = {
first: this.props.variables.first,
query: this.props.variables.query
};
search = (query: string) => {
if (query === undefined) {
this.setState({ query: "" });
} else {
this.setState({ query });
}
};
render() {
const { children, variables } = this.props;
return (
<Debounce debounceFn={this.search} time={200}>
{search => (
<Query
displayLoader={true}
variables={{
...variables,
query: this.state.query
}}
>
{result =>
children({
loadMore: () => loadMoreFn(result),
result,
search
})
}
</Query>
)}
</Debounce>
);
}
}
return BaseSearchComponent;
}
export default BaseSearch;

View file

@ -8,6 +8,7 @@ import AssignCategoriesDialog from "@saleor/components/AssignCategoryDialog";
import AssignCollectionDialog from "@saleor/components/AssignCollectionDialog"; import AssignCollectionDialog from "@saleor/components/AssignCollectionDialog";
import AssignProductDialog from "@saleor/components/AssignProductDialog"; import AssignProductDialog from "@saleor/components/AssignProductDialog";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA, PAGINATE_BY } from "@saleor/config";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
@ -16,12 +17,11 @@ import usePaginator, {
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { commonMessages, sectionNames } from "@saleor/intl"; import { commonMessages, sectionNames } from "@saleor/intl";
import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import useProductSearch from "@saleor/searches/useProductSearch";
import { categoryUrl } from "../../categories/urls"; import { categoryUrl } from "../../categories/urls";
import { collectionUrl } from "../../collections/urls"; import { collectionUrl } from "../../collections/urls";
import { DEFAULT_INITIAL_SEARCH_DATA, PAGINATE_BY } from "../../config";
import SearchCategories from "../../containers/SearchCategories";
import SearchCollections from "../../containers/SearchCollections";
import SearchProducts from "../../containers/SearchProducts";
import { decimal, getMutationState, joinDateTime, maybe } from "../../misc"; import { decimal, getMutationState, joinDateTime, maybe } from "../../misc";
import { productUrl } from "../../products/urls"; import { productUrl } from "../../products/urls";
import { DiscountValueTypeEnum, SaleType } from "../../types/globalTypes"; import { DiscountValueTypeEnum, SaleType } from "../../types/globalTypes";
@ -66,6 +66,24 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
params.ids params.ids
); );
const intl = useIntl(); const intl = useIntl();
const {
search: searchCategories,
result: searchCategoriesOpts
} = useCategorySearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
search: searchCollections,
result: searchCollectionsOpts
} = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
search: searchProducts,
result: searchProductsOpts
} = useProductSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const paginationState = createPaginationState(PAGINATE_BY, params); const paginationState = createPaginationState(PAGINATE_BY, params);
const changeTab = (tab: SaleDetailsPageTab) => { const changeTab = (tab: SaleDetailsPageTab) => {
@ -341,116 +359,87 @@ export const SaleDetails: React.FC<SaleDetailsProps> = ({ id, params }) => {
toggle={toggle} toggle={toggle}
toggleAll={toggleAll} toggleAll={toggleAll}
/> />
<SearchProducts <AssignProductDialog
variables={DEFAULT_INITIAL_SEARCH_DATA} confirmButtonState={assignTransitionState}
> open={params.action === "assign-product"}
{({ onFetch={searchProducts}
search: searchProducts, loading={searchProductsOpts.loading}
result: searchProductsOpts onClose={closeModal}
}) => ( onSubmit={products =>
<AssignProductDialog saleCataloguesAdd({
confirmButtonState={assignTransitionState} variables: {
open={params.action === "assign-product"} ...paginationState,
onFetch={searchProducts} id,
loading={searchProductsOpts.loading} input: {
onClose={closeModal} products: products.map(
onSubmit={products => product => product.id
saleCataloguesAdd({
variables: {
...paginationState,
id,
input: {
products: products.map(
product => product.id
)
}
}
})
}
products={maybe(() =>
searchProductsOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedProduct => suggestedProduct.id
) )
)} }
/>
)}
</SearchProducts>
<SearchCategories
variables={DEFAULT_INITIAL_SEARCH_DATA}
>
{({
search: searchCategories,
result: searchCategoriesOpts
}) => (
<AssignCategoriesDialog
categories={maybe(() =>
searchCategoriesOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedCategory =>
suggestedCategory.id
)
)}
confirmButtonState={assignTransitionState}
open={params.action === "assign-category"}
onFetch={searchCategories}
loading={searchCategoriesOpts.loading}
onClose={closeModal}
onSubmit={categories =>
saleCataloguesAdd({
variables: {
...paginationState,
id,
input: {
categories: categories.map(
product => product.id
)
}
}
})
} }
/> })
}
products={maybe(() =>
searchProductsOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedProduct => suggestedProduct.id
)
)} )}
</SearchCategories> />
<SearchCollections <AssignCategoriesDialog
variables={DEFAULT_INITIAL_SEARCH_DATA} categories={maybe(() =>
> searchCategoriesOpts.data.search.edges
{({ .map(edge => edge.node)
search: searchCollections, .filter(
result: searchCollectionsOpts suggestedCategory => suggestedCategory.id
}) => ( )
<AssignCollectionDialog )}
collections={maybe(() => confirmButtonState={assignTransitionState}
searchCollectionsOpts.data.search.edges open={params.action === "assign-category"}
.map(edge => edge.node) onFetch={searchCategories}
.filter( loading={searchCategoriesOpts.loading}
suggestedCategory => onClose={closeModal}
suggestedCategory.id onSubmit={categories =>
saleCataloguesAdd({
variables: {
...paginationState,
id,
input: {
categories: categories.map(
product => product.id
) )
)} }
confirmButtonState={assignTransitionState}
open={params.action === "assign-collection"}
onFetch={searchCollections}
loading={searchCollectionsOpts.loading}
onClose={closeModal}
onSubmit={collections =>
saleCataloguesAdd({
variables: {
...paginationState,
id,
input: {
collections: collections.map(
product => product.id
)
}
}
})
} }
/> })
}
/>
<AssignCollectionDialog
collections={maybe(() =>
searchCollectionsOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedCategory => suggestedCategory.id
)
)} )}
</SearchCollections> confirmButtonState={assignTransitionState}
open={params.action === "assign-collection"}
onFetch={searchCollections}
loading={searchCollectionsOpts.loading}
onClose={closeModal}
onSubmit={collections =>
saleCataloguesAdd({
variables: {
...paginationState,
id,
input: {
collections: collections.map(
product => product.id
)
}
}
})
}
/>
<ActionDialog <ActionDialog
open={ open={
params.action === "unassign-category" && params.action === "unassign-category" &&

View file

@ -8,6 +8,7 @@ import AssignCategoriesDialog from "@saleor/components/AssignCategoryDialog";
import AssignCollectionDialog from "@saleor/components/AssignCollectionDialog"; import AssignCollectionDialog from "@saleor/components/AssignCollectionDialog";
import AssignProductDialog from "@saleor/components/AssignProductDialog"; import AssignProductDialog from "@saleor/components/AssignProductDialog";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA, PAGINATE_BY } from "@saleor/config";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
@ -16,12 +17,11 @@ import usePaginator, {
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { commonMessages, sectionNames } from "@saleor/intl"; import { commonMessages, sectionNames } from "@saleor/intl";
import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import useProductSearch from "@saleor/searches/useProductSearch";
import { categoryUrl } from "../../categories/urls"; import { categoryUrl } from "../../categories/urls";
import { collectionUrl } from "../../collections/urls"; import { collectionUrl } from "../../collections/urls";
import { DEFAULT_INITIAL_SEARCH_DATA, PAGINATE_BY } from "../../config";
import SearchCategories from "../../containers/SearchCategories";
import SearchCollections from "../../containers/SearchCollections";
import SearchProducts from "../../containers/SearchProducts";
import { decimal, getMutationState, joinDateTime, maybe } from "../../misc"; import { decimal, getMutationState, joinDateTime, maybe } from "../../misc";
import { productUrl } from "../../products/urls"; import { productUrl } from "../../products/urls";
import { import {
@ -68,6 +68,24 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
params.ids params.ids
); );
const intl = useIntl(); const intl = useIntl();
const {
search: searchCategories,
result: searchCategoriesOpts
} = useCategorySearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
search: searchCollections,
result: searchCollectionsOpts
} = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
search: searchProducts,
result: searchProductsOpts
} = useProductSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const paginationState = createPaginationState(PAGINATE_BY, params); const paginationState = createPaginationState(PAGINATE_BY, params);
const changeTab = (tab: VoucherDetailsPageTab) => { const changeTab = (tab: VoucherDetailsPageTab) => {
@ -420,80 +438,60 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
toggle={toggle} toggle={toggle}
toggleAll={toggleAll} toggleAll={toggleAll}
/> />
<SearchCategories <AssignCategoriesDialog
variables={DEFAULT_INITIAL_SEARCH_DATA} categories={maybe(() =>
> searchCategoriesOpts.data.search.edges
{({ .map(edge => edge.node)
search: searchCategories, .filter(
result: searchCategoriesOpts suggestedCategory => suggestedCategory.id
}) => ( )
<AssignCategoriesDialog
categories={maybe(() =>
searchCategoriesOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedCategory =>
suggestedCategory.id
)
)}
confirmButtonState={assignTransitionState}
open={params.action === "assign-category"}
onFetch={searchCategories}
loading={searchCategoriesOpts.loading}
onClose={closeModal}
onSubmit={categories =>
voucherCataloguesAdd({
variables: {
...paginationState,
id,
input: {
categories: categories.map(
product => product.id
)
}
}
})
}
/>
)} )}
</SearchCategories> confirmButtonState={assignTransitionState}
<SearchCollections open={params.action === "assign-category"}
variables={DEFAULT_INITIAL_SEARCH_DATA} onFetch={searchCategories}
> loading={searchCategoriesOpts.loading}
{({ onClose={closeModal}
search: searchCollections, onSubmit={categories =>
result: searchCollectionsOpts voucherCataloguesAdd({
}) => ( variables: {
<AssignCollectionDialog ...paginationState,
collections={maybe(() => id,
searchCollectionsOpts.data.search.edges input: {
.map(edge => edge.node) categories: categories.map(
.filter( product => product.id
suggestedCategory =>
suggestedCategory.id
) )
)} }
confirmButtonState={assignTransitionState}
open={params.action === "assign-collection"}
onFetch={searchCollections}
loading={searchCollectionsOpts.loading}
onClose={closeModal}
onSubmit={collections =>
voucherCataloguesAdd({
variables: {
...paginationState,
id,
input: {
collections: collections.map(
product => product.id
)
}
}
})
} }
/> })
}
/>
<AssignCollectionDialog
collections={maybe(() =>
searchCollectionsOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedCategory => suggestedCategory.id
)
)} )}
</SearchCollections> confirmButtonState={assignTransitionState}
open={params.action === "assign-collection"}
onFetch={searchCollections}
loading={searchCollectionsOpts.loading}
onClose={closeModal}
onSubmit={collections =>
voucherCataloguesAdd({
variables: {
...paginationState,
id,
input: {
collections: collections.map(
product => product.id
)
}
}
})
}
/>
<DiscountCountrySelectDialog <DiscountCountrySelectDialog
confirmButtonState={formTransitionState} confirmButtonState={formTransitionState}
countries={maybe(() => shop.countries, [])} countries={maybe(() => shop.countries, [])}
@ -517,42 +515,33 @@ export const VoucherDetails: React.FC<VoucherDetailsProps> = ({
[] []
)} )}
/> />
<SearchProducts <AssignProductDialog
variables={DEFAULT_INITIAL_SEARCH_DATA} confirmButtonState={assignTransitionState}
> open={params.action === "assign-product"}
{({ onFetch={searchProducts}
search: searchProducts, loading={searchProductsOpts.loading}
result: searchProductsOpts onClose={closeModal}
}) => ( onSubmit={products =>
<AssignProductDialog voucherCataloguesAdd({
confirmButtonState={assignTransitionState} variables: {
open={params.action === "assign-product"} ...paginationState,
onFetch={searchProducts} id,
loading={searchProductsOpts.loading} input: {
onClose={closeModal} products: products.map(
onSubmit={products => product => product.id
voucherCataloguesAdd({
variables: {
...paginationState,
id,
input: {
products: products.map(
product => product.id
)
}
}
})
}
products={maybe(() =>
searchProductsOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedProduct => suggestedProduct.id
) )
)} }
/> }
})
}
products={maybe(() =>
searchProductsOpts.data.search.edges
.map(edge => edge.node)
.filter(
suggestedProduct => suggestedProduct.id
)
)} )}
</SearchProducts> />
<ActionDialog <ActionDialog
open={ open={
params.action === "unassign-category" && params.action === "unassign-category" &&

View file

@ -16,7 +16,7 @@ export interface LoadMore<TData, TVariables> {
) => Promise<ApolloQueryResult<TData>>; ) => Promise<ApolloQueryResult<TData>>;
} }
type UseQuery<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<TData, TVariables> = Partial<{
displayLoader: boolean; displayLoader: boolean;
@ -26,17 +26,17 @@ type UseQueryOpts<TData, TVariables> = Partial<{
}>; }>;
type UseQueryHook<TData, TVariables> = ( type UseQueryHook<TData, TVariables> = (
opts: UseQueryOpts<TData, TVariables> opts: UseQueryOpts<TData, TVariables>
) => UseQuery<TData, TVariables>; ) => UseQueryResult<TData, TVariables>;
function makeQuery<TData, TVariables>( function makeQuery<TData, TVariables>(
query: DocumentNode query: DocumentNode
): UseQueryHook<TData, TVariables> { ): UseQueryHook<TData, TVariables> {
function useQuery<TData, TVariables>({ function useQuery({
displayLoader, displayLoader,
require, require,
skip, skip,
variables variables
}: UseQueryOpts<TData, TVariables>): UseQuery<TData, TVariables> { }: UseQueryOpts<TData, TVariables>): UseQueryResult<TData, TVariables> {
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
const [, dispatchAppState] = useAppState(); const [, dispatchAppState] = useAppState();

57
src/hooks/makeSearch.ts Normal file
View file

@ -0,0 +1,57 @@
import { DocumentNode } from "graphql";
import { useState } from "react";
import { QueryResult } from "react-apollo";
import makeQuery, { UseQueryResult } from "./makeQuery";
import useDebounce from "./useDebounce";
export interface SearchVariables {
after?: string;
first: number;
query: string;
}
export interface UseSearchResult<TData, TVariables extends SearchVariables> {
loadMore: () => void;
result: QueryResult<TData, TVariables>;
search: (query: string) => void;
}
export type UseSearchOpts<TVariables extends SearchVariables> = Partial<{
skip: boolean;
variables: TVariables;
}>;
export type UseSearchHook<TData, TVariables extends SearchVariables> = (
opts: UseSearchOpts<TVariables>
) => UseSearchResult<TData, TVariables>;
function makeSearch<TData, TVariables extends SearchVariables>(
query: DocumentNode,
loadMoreFn: (result: UseQueryResult<TData, TVariables>) => void
): UseSearchHook<TData, TVariables> {
const useSearchQuery = makeQuery<TData, TVariables>(query);
function useSearch(
opts: UseSearchOpts<TVariables>
): UseSearchResult<TData, TVariables> {
const [searchQuery, setSearchQuery] = useState("");
const debouncedSearch = useDebounce(setSearchQuery);
const result = useSearchQuery({
...opts,
displayLoader: true,
variables: {
...opts.variables,
query: searchQuery
}
});
return {
loadMore: () => loadMoreFn(result),
result,
search: debouncedSearch
};
}
return useSearch;
}
export default makeSearch;

View file

@ -1,9 +1,9 @@
import { DocumentNode } from "graphql"; import { DocumentNode } from "graphql";
import { PageInfoFragment } from "@saleor/types/PageInfoFragment"; import { PageInfoFragment } from "@saleor/types/PageInfoFragment";
import BaseSearch, { SearchQueryVariables } from "./BaseSearch"; import makeSearch, { SearchVariables, UseSearchHook } from "./makeSearch";
export interface SearchQuery { export interface SearchData {
search: { search: {
edges: Array<{ edges: Array<{
node: any; node: any;
@ -12,11 +12,11 @@ export interface SearchQuery {
}; };
} }
function TopLevelSearch< function makeTopLevelSearch<
TQuery extends SearchQuery, TData extends SearchData,
TQueryVariables extends SearchQueryVariables TVariables extends SearchVariables
>(query: DocumentNode) { >(query: DocumentNode): UseSearchHook<TData, TVariables> {
return BaseSearch<TQuery, TQueryVariables>(query, result => { return makeSearch<TData, TVariables>(query, result => {
if (result.data.search.pageInfo.hasNextPage) { if (result.data.search.pageInfo.hasNextPage) {
result.loadMore( result.loadMore(
(prev, next) => { (prev, next) => {
@ -44,4 +44,4 @@ function TopLevelSearch<
}); });
} }
export default TopLevelSearch; export default makeTopLevelSearch;

19
src/hooks/useDebounce.ts Normal file
View file

@ -0,0 +1,19 @@
import { useEffect, useRef } from "react";
export type UseDebounceFn<T> = (...args: T[]) => void;
function useDebounce<T>(
debounceFn: UseDebounceFn<T>,
time = 200
): UseDebounceFn<T> {
const timer = useRef(null);
useEffect(() => () => clearTimeout(timer.current));
return (...args: T[]) => {
if (timer.current) {
clearTimeout(timer.current);
}
timer.current = setTimeout(() => debounceFn(...args), time);
};
}
export default useDebounce;

View file

@ -14,13 +14,13 @@ import ConfirmButton, {
ConfirmButtonTransitionState ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton"; } from "@saleor/components/ConfirmButton";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";
import { SearchCategories_search_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections";
import { SearchPages_search_edges_node } from "@saleor/containers/SearchPages/types/SearchPages";
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen"; import useModalDialogOpen from "@saleor/hooks/useModalDialogOpen";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { buttonMessages, sectionNames } from "@saleor/intl"; import { buttonMessages, sectionNames } from "@saleor/intl";
import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { UserError } from "@saleor/types"; import { UserError } from "@saleor/types";
import { getErrors, getFieldError } from "@saleor/utils/errors"; import { getErrors, getFieldError } from "@saleor/utils/errors";
import { getMenuItemByValue, IMenu } from "@saleor/utils/menu"; import { getMenuItemByValue, IMenu } from "@saleor/utils/menu";

View file

@ -3,14 +3,14 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
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 useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import usePageSearch from "@saleor/searches/usePageSearch";
import { categoryUrl } from "../../../categories/urls"; import { categoryUrl } from "../../../categories/urls";
import { collectionUrl } from "../../../collections/urls"; import { collectionUrl } from "../../../collections/urls";
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config";
import SearchCategories from "../../../containers/SearchCategories";
import SearchCollections from "../../../containers/SearchCollections";
import SearchPages from "../../../containers/SearchPages";
import { getMutationState, maybe } from "../../../misc"; import { getMutationState, maybe } from "../../../misc";
import { pageUrl } from "../../../pages/urls"; import { pageUrl } from "../../../pages/urls";
import MenuDetailsPage, { import MenuDetailsPage, {
@ -59,6 +59,15 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
const categorySearch = useCategorySearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const collectionSearch = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const pageSearch = usePageSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const closeModal = () => const closeModal = () =>
navigate( navigate(
@ -95,328 +104,267 @@ const MenuDetails: React.FC<MenuDetailsProps> = ({ id, params }) => {
}; };
return ( return (
<SearchPages variables={DEFAULT_INITIAL_SEARCH_DATA}> <MenuDetailsQuery displayLoader variables={{ id }}>
{pageSearch => ( {({ data, loading, refetch }) => {
<SearchCategories variables={DEFAULT_INITIAL_SEARCH_DATA}> const handleQueryChange = (query: string) => {
{categorySearch => ( categorySearch.search(query);
<SearchCollections variables={DEFAULT_INITIAL_SEARCH_DATA}> collectionSearch.search(query);
{collectionSearch => ( pageSearch.search(query);
<MenuDetailsQuery displayLoader variables={{ id }}> };
{({ data, loading, refetch }) => {
const handleQueryChange = (query: string) => {
categorySearch.search(query);
collectionSearch.search(query);
pageSearch.search(query);
};
const categories = maybe( const categories = maybe(
() => () => categorySearch.result.data.search.edges.map(edge => edge.node),
categorySearch.result.data.search.edges.map( []
edge => edge.node );
),
[]
);
const collections = maybe( const collections = maybe(
() => () =>
collectionSearch.result.data.search.edges.map( collectionSearch.result.data.search.edges.map(edge => edge.node),
edge => edge.node []
), );
[]
);
const pages = maybe( const pages = maybe(
() => () => pageSearch.result.data.search.edges.map(edge => edge.node),
pageSearch.result.data.search.edges.map( []
edge => edge.node );
),
[]
);
return ( return (
<MenuDeleteMutation <MenuDeleteMutation
onCompleted={data => handleDelete(data, navigate, notify, intl)}
>
{(menuDelete, menuDeleteOpts) => (
<MenuUpdateMutation
onCompleted={data => handleUpdate(data, notify, refetch, intl)}
>
{(menuUpdate, menuUpdateOpts) => {
const deleteState = getMutationState(
menuDeleteOpts.called,
menuDeleteOpts.loading,
maybe(() => menuDeleteOpts.data.menuDelete.errors)
);
const updateState = getMutationState(
menuUpdateOpts.called,
menuUpdateOpts.loading,
maybe(() => menuUpdateOpts.data.menuUpdate.errors),
maybe(() => menuUpdateOpts.data.menuItemMove.errors)
);
// This is a workaround to let know <MenuDetailsPage />
// that it should clean operation stack if mutations
// were successful
const handleSubmit = async (data: MenuDetailsSubmitData) => {
try {
const result = await menuUpdate({
variables: {
id,
moves: getMoves(data),
name: data.name,
removeIds: getRemoveIds(data)
}
});
if (result) {
if (
result.data.menuItemBulkDelete.errors.length > 0 ||
result.data.menuItemMove.errors.length > 0 ||
result.data.menuUpdate.errors.length > 0
) {
return false;
}
}
return true;
} catch {
return false;
}
};
return (
<>
<MenuDetailsPage
disabled={loading}
menu={maybe(() => data.menu)}
onBack={() => navigate(menuListUrl())}
onDelete={() =>
navigate(
menuUrl(id, {
action: "remove"
})
)
}
onItemAdd={() =>
navigate(
menuUrl(id, {
action: "add-item"
})
)
}
onItemClick={handleItemClick}
onItemEdit={itemId =>
navigate(
menuUrl(id, {
action: "edit-item",
id: itemId
})
)
}
onSubmit={handleSubmit}
saveButtonState={updateState}
/>
<ActionDialog
open={params.action === "remove"}
onClose={closeModal}
confirmButtonState={deleteState}
onConfirm={() => menuDelete({ variables: { id } })}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete Menu",
description: "dialog header",
id: "menuDetailsDeleteMenuHeader"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete menu {menuName}?"
id="menuDetailsDeleteMenuContent"
values={{
menuName: (
<strong>
{maybe(() => data.menu.name, "...")}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<MenuItemCreateMutation
onCompleted={data => onCompleted={data =>
handleDelete(data, navigate, notify, intl) handleItemCreate(data, notify, closeModal, intl)
} }
> >
{(menuDelete, menuDeleteOpts) => ( {(menuItemCreate, menuItemCreateOpts) => {
<MenuUpdateMutation const handleSubmit = (
onCompleted={data => data: MenuItemDialogFormData
handleUpdate(data, notify, refetch, intl) ) => {
} const variables: MenuItemCreateVariables = {
> input: getMenuItemCreateInputData(id, data)
{(menuUpdate, menuUpdateOpts) => { };
const deleteState = getMutationState(
menuDeleteOpts.called,
menuDeleteOpts.loading,
maybe(
() => menuDeleteOpts.data.menuDelete.errors
)
);
const updateState = getMutationState( menuItemCreate({ variables });
menuUpdateOpts.called, };
menuUpdateOpts.loading,
maybe(
() => menuUpdateOpts.data.menuUpdate.errors
),
maybe(
() => menuUpdateOpts.data.menuItemMove.errors
)
);
// This is a workaround to let know <MenuDetailsPage /> const formTransitionState = getMutationState(
// that it should clean operation stack if mutations menuItemCreateOpts.called,
// were successful menuItemCreateOpts.loading,
const handleSubmit = async ( maybe(
data: MenuDetailsSubmitData () =>
) => { menuItemCreateOpts.data.menuItemCreate.errors
try { )
const result = await menuUpdate({ );
variables: {
id,
moves: getMoves(data),
name: data.name,
removeIds: getRemoveIds(data)
}
});
if (result) {
if (
result.data.menuItemBulkDelete.errors
.length > 0 ||
result.data.menuItemMove.errors.length >
0 ||
result.data.menuUpdate.errors.length > 0
) {
return false;
}
}
return true;
} catch {
return false;
}
};
return ( return (
<> <MenuItemDialog
<MenuDetailsPage open={params.action === "add-item"}
disabled={loading} categories={categories}
menu={maybe(() => data.menu)} collections={collections}
onBack={() => navigate(menuListUrl())} errors={maybe(
onDelete={() => () =>
navigate( menuItemCreateOpts.data.menuItemCreate.errors,
menuUrl(id, { []
action: "remove" )}
}) pages={pages}
) loading={
} categorySearch.result.loading ||
onItemAdd={() => collectionSearch.result.loading
navigate( }
menuUrl(id, { confirmButtonState={formTransitionState}
action: "add-item" disabled={menuItemCreateOpts.loading}
}) onClose={closeModal}
) onSubmit={handleSubmit}
} onQueryChange={handleQueryChange}
onItemClick={handleItemClick} />
onItemEdit={itemId => );
navigate( }}
menuUrl(id, { </MenuItemCreateMutation>
action: "edit-item", <MenuItemUpdateMutation
id: itemId onCompleted={data =>
}) handleItemUpdate(data, id, navigate, notify, intl)
) }
} >
onSubmit={handleSubmit} {(menuItemUpdate, menuItemUpdateOpts) => {
saveButtonState={updateState} const handleSubmit = (
/> data: MenuItemDialogFormData
<ActionDialog ) => {
open={params.action === "remove"} const variables: MenuItemUpdateVariables = {
onClose={closeModal} id: params.id,
confirmButtonState={deleteState} input: getMenuItemInputData(data)
onConfirm={() => };
menuDelete({ variables: { id } })
}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete Menu",
description: "dialog header",
id: "menuDetailsDeleteMenuHeader"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete menu {menuName}?"
id="menuDetailsDeleteMenuContent"
values={{
menuName: (
<strong>
{maybe(
() => data.menu.name,
"..."
)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<MenuItemCreateMutation menuItemUpdate({ variables });
onCompleted={data => };
handleItemCreate(
data,
notify,
closeModal,
intl
)
}
>
{(menuItemCreate, menuItemCreateOpts) => {
const handleSubmit = (
data: MenuItemDialogFormData
) => {
const variables: MenuItemCreateVariables = {
input: getMenuItemCreateInputData(
id,
data
)
};
menuItemCreate({ variables }); const menuItem = maybe(() =>
}; getNode(
data.menu.items,
findNode(data.menu.items, params.id)
)
);
const formTransitionState = getMutationState( const formTransitionState = getMutationState(
menuItemCreateOpts.called, menuItemUpdateOpts.called,
menuItemCreateOpts.loading, menuItemUpdateOpts.loading,
maybe( maybe(
() => () =>
menuItemCreateOpts.data menuItemUpdateOpts.data.menuItemUpdate.errors
.menuItemCreate.errors )
) );
);
return ( const initialFormData: MenuItemDialogFormData = {
<MenuItemDialog id: maybe(() => getItemId(menuItem)),
open={params.action === "add-item"} name: maybe(() => menuItem.name, "..."),
categories={categories} type: maybe<MenuItemType>(
collections={collections} () => getItemType(menuItem),
errors={maybe( "category"
() => )
menuItemCreateOpts.data };
.menuItemCreate.errors,
[]
)}
pages={pages}
loading={
categorySearch.result.loading ||
collectionSearch.result.loading
}
confirmButtonState={
formTransitionState
}
disabled={menuItemCreateOpts.loading}
onClose={closeModal}
onSubmit={handleSubmit}
onQueryChange={handleQueryChange}
/>
);
}}
</MenuItemCreateMutation>
<MenuItemUpdateMutation
onCompleted={data =>
handleItemUpdate(
data,
id,
navigate,
notify,
intl
)
}
>
{(menuItemUpdate, menuItemUpdateOpts) => {
const handleSubmit = (
data: MenuItemDialogFormData
) => {
const variables: MenuItemUpdateVariables = {
id: params.id,
input: getMenuItemInputData(data)
};
menuItemUpdate({ variables }); return (
}; <MenuItemDialog
open={params.action === "edit-item"}
const menuItem = maybe(() => categories={categories}
getNode( collections={collections}
data.menu.items, errors={maybe(
findNode(data.menu.items, params.id) () =>
) menuItemUpdateOpts.data.menuItemUpdate.errors,
); []
)}
const formTransitionState = getMutationState( pages={pages}
menuItemUpdateOpts.called, initial={initialFormData}
menuItemUpdateOpts.loading, initialDisplayValue={getInitialDisplayValue(
maybe( menuItem
() => )}
menuItemUpdateOpts.data loading={
.menuItemUpdate.errors categorySearch.result.loading ||
) collectionSearch.result.loading
); }
confirmButtonState={formTransitionState}
const initialFormData: MenuItemDialogFormData = { disabled={menuItemUpdateOpts.loading}
id: maybe(() => getItemId(menuItem)), onClose={closeModal}
name: maybe(() => menuItem.name, "..."), onSubmit={handleSubmit}
type: maybe<MenuItemType>( onQueryChange={handleQueryChange}
() => getItemType(menuItem), />
"category" );
) }}
}; </MenuItemUpdateMutation>
</>
return ( );
<MenuItemDialog }}
open={params.action === "edit-item"} </MenuUpdateMutation>
categories={categories} )}
collections={collections} </MenuDeleteMutation>
errors={maybe( );
() => }}
menuItemUpdateOpts.data </MenuDetailsQuery>
.menuItemUpdate.errors,
[]
)}
pages={pages}
initial={initialFormData}
initialDisplayValue={getInitialDisplayValue(
menuItem
)}
loading={
categorySearch.result.loading ||
collectionSearch.result.loading
}
confirmButtonState={
formTransitionState
}
disabled={menuItemUpdateOpts.loading}
onClose={closeModal}
onSubmit={handleSubmit}
onQueryChange={handleQueryChange}
/>
);
}}
</MenuItemUpdateMutation>
</>
);
}}
</MenuUpdateMutation>
)}
</MenuDeleteMutation>
);
}}
</MenuDetailsQuery>
)}
</SearchCollections>
)}
</SearchCategories>
)}
</SearchPages>
); );
}; };
MenuDetails.displayName = "MenuDetails"; MenuDetails.displayName = "MenuDetails";

View file

@ -16,10 +16,10 @@ import SingleAutocompleteSelectField from "@saleor/components/SingleAutocomplete
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { SearchCustomers_search_edges_node } from "@saleor/searches/types/SearchCustomers";
import { FetchMoreProps, UserPermissionProps } from "@saleor/types"; import { FetchMoreProps, UserPermissionProps } from "@saleor/types";
import { PermissionEnum } from "@saleor/types/globalTypes"; import { PermissionEnum } from "@saleor/types/globalTypes";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { SearchCustomers_search_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers";
import { customerUrl } from "../../../customers/urls"; import { customerUrl } from "../../../customers/urls";
import { createHref, maybe } from "../../../misc"; import { createHref, maybe } from "../../../misc";
import { OrderDetails_order } from "../../types/OrderDetails"; import { OrderDetails_order } from "../../types/OrderDetails";

View file

@ -13,8 +13,8 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { SearchCustomers_search_edges_node } from "@saleor/searches/types/SearchCustomers";
import { FetchMoreProps, UserPermissionProps } from "@saleor/types"; import { FetchMoreProps, UserPermissionProps } from "@saleor/types";
import { SearchCustomers_search_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers";
import { maybe } from "../../../misc"; import { maybe } from "../../../misc";
import { DraftOrderInput } from "../../../types/globalTypes"; import { DraftOrderInput } from "../../../types/globalTypes";
import { OrderDetails_order } from "../../types/OrderDetails"; import { OrderDetails_order } from "../../types/OrderDetails";

View file

@ -1,5 +1,5 @@
import { SearchCustomers_search_edges_node } from "@saleor/searches/types/SearchCustomers";
import { MessageDescriptor } from "react-intl"; import { MessageDescriptor } from "react-intl";
import { SearchCustomers_search_edges_node } from "../containers/SearchCustomers/types/SearchCustomers";
import { transformOrderStatus, transformPaymentStatus } from "../misc"; import { transformOrderStatus, transformPaymentStatus } from "../misc";
import { import {
FulfillmentStatus, FulfillmentStatus,

View file

@ -1,6 +1,6 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import TopLevelSearch from "../containers/TopLevelSearch"; import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { TypedQuery } from "../queries"; import { TypedQuery } from "../queries";
import { OrderDetails, OrderDetailsVariables } from "./types/OrderDetails"; import { OrderDetails, OrderDetailsVariables } from "./types/OrderDetails";
import { import {
@ -314,7 +314,7 @@ export const searchOrderVariant = gql`
} }
} }
`; `;
export const SearchOrderVariant = TopLevelSearch< export const useOrderVariantSearch = makeTopLevelSearch<
SearchOrderVariantType, SearchOrderVariantType,
SearchOrderVariantVariables SearchOrderVariantVariables
>(searchOrderVariant); >(searchOrderVariant);

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,7 @@ import useSearchQuery from "@saleor/hooks/useSearchQuery";
import { buttonMessages } from "@saleor/intl"; import { buttonMessages } from "@saleor/intl";
import { maybe, renderCollection } from "@saleor/misc"; import { maybe, renderCollection } from "@saleor/misc";
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps } from "@saleor/types";
import { SearchAttributes_productType_availableAttributes_edges_node } from "../../containers/SearchAttributes/types/SearchAttributes"; import { SearchAttributes_productType_availableAttributes_edges_node } from "../../hooks/useAvailableAttributeSearch/types/SearchAttributes";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
actions: { actions: {

View file

@ -1,7 +1,7 @@
import { import {
SearchProductTypes_search_edges_node, SearchProductTypes_search_edges_node,
SearchProductTypes_search_edges_node_productAttributes SearchProductTypes_search_edges_node_productAttributes
} from "@saleor/containers/SearchProductTypes/types/SearchProductTypes"; } from "@saleor/searches/types/SearchProductTypes";
import { AttributeInputTypeEnum } from "../types/globalTypes"; import { AttributeInputTypeEnum } from "../types/globalTypes";
import { ProductTypeDetails_productType } from "./types/ProductTypeDetails"; import { ProductTypeDetails_productType } from "./types/ProductTypeDetails";
import { ProductTypeList_productTypes_edges_node } from "./types/ProductTypeList"; import { ProductTypeList_productTypes_edges_node } from "./types/ProductTypeList";

View file

@ -1,7 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeSearch from "@saleor/hooks/makeSearch";
import { pageInfoFragment } from "@saleor/queries"; import { pageInfoFragment } from "@saleor/queries";
import BaseSearch from "../../../containers/BaseSearch";
import { import {
SearchAttributes, SearchAttributes,
SearchAttributesVariables SearchAttributesVariables
@ -37,24 +37,33 @@ export const searchAttributes = gql`
} }
`; `;
export default BaseSearch<SearchAttributes, SearchAttributesVariables>( export default makeSearch<SearchAttributes, SearchAttributesVariables>(
searchAttributes, searchAttributes,
result => result =>
result.loadMore( result.loadMore(
(prev, next) => ({ (prev, next) => {
...prev, if (
productType: { prev.productType.availableAttributes.pageInfo.endCursor ===
...prev.productType, next.productType.availableAttributes.pageInfo.endCursor
availableAttributes: { ) {
...prev.productType.availableAttributes, return prev;
edges: [
...prev.productType.availableAttributes.edges,
...next.productType.availableAttributes.edges
],
pageInfo: next.productType.availableAttributes.pageInfo
}
} }
}),
return {
...prev,
productType: {
...prev.productType,
availableAttributes: {
...prev.productType.availableAttributes,
edges: [
...prev.productType.availableAttributes.edges,
...next.productType.availableAttributes.edges
],
pageInfo: next.productType.availableAttributes.pageInfo
}
}
};
},
{ {
after: result.data.productType.availableAttributes.pageInfo.endCursor after: result.data.productType.availableAttributes.pageInfo.endCursor
} }

View file

@ -4,6 +4,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import { attributeUrl } from "@saleor/attributes/urls"; import { attributeUrl } from "@saleor/attributes/urls";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
@ -19,7 +20,7 @@ import ProductTypeDetailsPage, {
ProductTypeForm ProductTypeForm
} from "../../components/ProductTypeDetailsPage"; } from "../../components/ProductTypeDetailsPage";
import ProductTypeOperations from "../../containers/ProductTypeOperations"; import ProductTypeOperations from "../../containers/ProductTypeOperations";
import SearchAttributes from "../../containers/SearchAttributes"; import useAvailableAttributeSearch from "../../hooks/useAvailableAttributeSearch";
import { TypedProductTypeDetailsQuery } from "../../queries"; import { TypedProductTypeDetailsQuery } from "../../queries";
import { AssignAttribute } from "../../types/AssignAttribute"; import { AssignAttribute } from "../../types/AssignAttribute";
import { ProductTypeDelete } from "../../types/ProductTypeDelete"; import { ProductTypeDelete } from "../../types/ProductTypeDelete";
@ -46,6 +47,12 @@ export const ProductTypeUpdate: React.FC<ProductTypeUpdateProps> = ({
const productAttributeListActions = useBulkActions(); const productAttributeListActions = useBulkActions();
const variantAttributeListActions = useBulkActions(); const variantAttributeListActions = useBulkActions();
const intl = useIntl(); const intl = useIntl();
const { loadMore, search, result } = useAvailableAttributeSearch({
variables: {
...DEFAULT_INITIAL_SEARCH_DATA,
id
}
});
return ( return (
<ProductTypeUpdateErrors> <ProductTypeUpdateErrors>
@ -330,109 +337,55 @@ export const ProductTypeUpdate: React.FC<ProductTypeUpdateProps> = ({
) )
}} }}
/> />
{!dataLoading && ( {!dataLoading &&
<SearchAttributes Object.keys(AttributeTypeEnum).map(key => (
variables={{ <AssignAttributeDialog
first: 15, attributes={maybe(() =>
id, result.data.productType.availableAttributes.edges.map(
query: "" edge => edge.node
}} )
> )}
{({ search, result }) => { confirmButtonState={assignTransactionState}
const fetchMore = () => errors={maybe(
result.loadMore( () =>
(prev, next) => { assignAttribute.opts.data.attributeAssign.errors.map(
if ( err => err.message
prev.productType.availableAttributes ),
.pageInfo.endCursor === []
next.productType.availableAttributes )}
.pageInfo.endCursor loading={result.loading}
) { onClose={closeModal}
return prev; onSubmit={handleAssignAttribute}
} onFetch={search}
return { onFetchMore={loadMore}
...prev, onOpen={result.refetch}
productType: { hasMore={maybe(
...prev.productType, () =>
availableAttributes: { result.data.productType.availableAttributes
...prev.productType.availableAttributes, .pageInfo.hasNextPage,
edges: [ false
...prev.productType )}
.availableAttributes.edges, open={
...next.productType params.action === "assign-attribute" &&
.availableAttributes.edges params.type === AttributeTypeEnum[key]
], }
pageInfo: selected={maybe(() => params.ids, [])}
next.productType.availableAttributes onToggle={attributeId => {
.pageInfo const ids = maybe(() => params.ids, []);
} navigate(
} productTypeUrl(id, {
}; ...params,
}, ids: ids.includes(attributeId)
{ ? params.ids.filter(
after: selectedId => selectedId !== attributeId
result.data.productType.availableAttributes
.pageInfo.endCursor
}
);
return (
<>
{Object.keys(AttributeTypeEnum).map(key => (
<AssignAttributeDialog
attributes={maybe(() =>
result.data.productType.availableAttributes.edges.map(
edge => edge.node
) )
)} : [...ids, attributeId]
confirmButtonState={assignTransactionState} })
errors={maybe( );
() => }}
assignAttribute.opts.data.attributeAssign.errors.map( key={key}
err => err.message />
), ))}
[]
)}
loading={result.loading}
onClose={closeModal}
onSubmit={handleAssignAttribute}
onFetch={search}
onFetchMore={fetchMore}
onOpen={result.refetch}
hasMore={maybe(
() =>
result.data.productType
.availableAttributes.pageInfo
.hasNextPage,
false
)}
open={
params.action === "assign-attribute" &&
params.type === AttributeTypeEnum[key]
}
selected={maybe(() => params.ids, [])}
onToggle={attributeId => {
const ids = maybe(() => params.ids, []);
navigate(
productTypeUrl(id, {
...params,
ids: ids.includes(attributeId)
? params.ids.filter(
selectedId =>
selectedId !== attributeId
)
: [...ids, attributeId]
})
);
}}
key={key}
/>
))}
</>
);
}}
</SearchAttributes>
)}
<ProductTypeDeleteDialog <ProductTypeDeleteDialog
confirmButtonState={deleteTransactionState} confirmButtonState={deleteTransactionState}
name={maybe(() => data.productType.name, "...")} name={maybe(() => data.productType.name, "...")}

View file

@ -13,9 +13,6 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
import VisibilityCard from "@saleor/components/VisibilityCard"; import VisibilityCard from "@saleor/components/VisibilityCard";
import { SearchCategories_search_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections";
import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/containers/SearchProductTypes/types/SearchProductTypes";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import useFormset from "@saleor/hooks/useFormset"; import useFormset from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
@ -25,6 +22,9 @@ import {
ProductAttributeValueChoices, ProductAttributeValueChoices,
ProductType ProductType
} from "@saleor/products/utils/data"; } from "@saleor/products/utils/data";
import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/searches/types/SearchProductTypes";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import { FetchMoreProps, UserError } from "../../../types"; import { FetchMoreProps, UserError } from "../../../types";

View file

@ -12,13 +12,13 @@ import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import SeoForm from "@saleor/components/SeoForm"; import SeoForm from "@saleor/components/SeoForm";
import VisibilityCard from "@saleor/components/VisibilityCard"; import VisibilityCard from "@saleor/components/VisibilityCard";
import { SearchCategories_search_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections";
import useDateLocalize from "@saleor/hooks/useDateLocalize"; import useDateLocalize from "@saleor/hooks/useDateLocalize";
import useFormset from "@saleor/hooks/useFormset"; import useFormset from "@saleor/hooks/useFormset";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories";
import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections";
import { FetchMoreProps, ListActions, UserError } from "@saleor/types"; import { FetchMoreProps, ListActions, UserError } from "@saleor/types";
import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";

View file

@ -2,13 +2,13 @@ import { RawDraftContentState } from "draft-js";
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/containers/SearchProductTypes/types/SearchProductTypes";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { import {
ProductDetails_product, ProductDetails_product,
ProductDetails_product_collections, ProductDetails_product_collections,
ProductDetails_product_variants ProductDetails_product_variants
} from "@saleor/products/types/ProductDetails"; } from "@saleor/products/types/ProductDetails";
import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/searches/types/SearchProductTypes";
import { ProductAttributeInput } from "../components/ProductAttributes"; import { ProductAttributeInput } from "../components/ProductAttributes";
import { VariantAttributeInput } from "../components/ProductVariantAttributes"; import { VariantAttributeInput } from "../components/ProductVariantAttributes";
import { ProductVariant } from "../types/ProductVariant"; import { ProductVariant } from "../types/ProductVariant";

View file

@ -2,13 +2,13 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import SearchProductTypes from "@saleor/containers/SearchProductTypes"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
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 { DEFAULT_INITIAL_SEARCH_DATA } from "../../config"; import useCategorySearch from "@saleor/searches/useCategorySearch";
import SearchCategories from "../../containers/SearchCategories"; import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import SearchCollections from "../../containers/SearchCollections"; import useProductTypeSearch from "@saleor/searches/useProductTypeSearch";
import { decimal, getMutationState, maybe } from "../../misc"; import { decimal, getMutationState, maybe } from "../../misc";
import ProductCreatePage, { import ProductCreatePage, {
ProductCreatePageSubmitData ProductCreatePageSubmitData
@ -26,174 +26,152 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = () => {
const notify = useNotifier(); const notify = useNotifier();
const shop = useShop(); const shop = useShop();
const intl = useIntl(); const intl = useIntl();
const {
loadMore: loadMoreCategories,
search: searchCategory,
result: searchCategoryOpts
} = useCategorySearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
loadMore: loadMoreCollections,
search: searchCollection,
result: searchCollectionOpts
} = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
loadMore: loadMoreProductTypes,
search: searchProductTypes,
result: searchProductTypesOpts
} = useProductTypeSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const handleBack = () => navigate(productListUrl()); const handleBack = () => navigate(productListUrl());
const handleSuccess = (data: ProductCreate) => {
if (data.productCreate.errors.length === 0) {
notify({
text: intl.formatMessage({
defaultMessage: "Product created"
})
});
navigate(productUrl(data.productCreate.product.id));
} else {
const attributeError = data.productCreate.errors.find(
err => err.field === "attributes"
);
if (!!attributeError) {
notify({ text: attributeError.message });
}
}
};
return ( return (
<SearchCategories variables={DEFAULT_INITIAL_SEARCH_DATA}> <TypedProductCreateMutation onCompleted={handleSuccess}>
{({ {(
loadMore: loadMoreCategories, productCreate,
search: searchCategory, {
result: searchCategoryOpts called: productCreateCalled,
}) => ( data: productCreateData,
<SearchCollections variables={DEFAULT_INITIAL_SEARCH_DATA}> loading: productCreateDataLoading
{({ }
loadMore: loadMoreCollections, ) => {
search: searchCollection, const handleSubmit = (formData: ProductCreatePageSubmitData) => {
result: searchCollectionOpts productCreate({
}) => ( variables: {
<SearchProductTypes variables={DEFAULT_INITIAL_SEARCH_DATA}> attributes: formData.attributes.map(attribute => ({
{({ id: attribute.id,
loadMore: loadMoreProductTypes, values: attribute.value
search: searchProductTypes, })),
result: searchProductTypesOpts basePrice: decimal(formData.basePrice),
}) => { category: formData.category,
const handleSuccess = (data: ProductCreate) => { chargeTaxes: formData.chargeTaxes,
if (data.productCreate.errors.length === 0) { collections: formData.collections,
notify({ descriptionJson: JSON.stringify(formData.description),
text: intl.formatMessage({ isPublished: formData.isPublished,
defaultMessage: "Product created" name: formData.name,
}) productType: formData.productType,
}); publicationDate:
navigate(productUrl(data.productCreate.product.id)); formData.publicationDate !== ""
} else { ? formData.publicationDate
const attributeError = data.productCreate.errors.find( : null,
err => err.field === "attributes" seo: {
); description: formData.seoDescription,
if (!!attributeError) { title: formData.seoTitle
notify({ text: attributeError.message }); },
} sku: formData.sku,
} stockQuantity:
}; formData.stockQuantity !== null ? formData.stockQuantity : 0
}
});
};
return ( const formTransitionState = getMutationState(
<TypedProductCreateMutation onCompleted={handleSuccess}> productCreateCalled,
{( productCreateDataLoading,
productCreate, maybe(() => productCreateData.productCreate.errors)
{ );
called: productCreateCalled, return (
data: productCreateData, <>
loading: productCreateDataLoading <WindowTitle
} title={intl.formatMessage({
) => { defaultMessage: "Create Product",
const handleSubmit = ( description: "window title"
formData: ProductCreatePageSubmitData })}
) => { />
productCreate({ <ProductCreatePage
variables: { currency={maybe(() => shop.defaultCurrency)}
attributes: formData.attributes.map(attribute => ({ categories={maybe(
id: attribute.id, () => searchCategoryOpts.data.search.edges,
values: attribute.value []
})), ).map(edge => edge.node)}
basePrice: decimal(formData.basePrice), collections={maybe(
category: formData.category, () => searchCollectionOpts.data.search.edges,
chargeTaxes: formData.chargeTaxes, []
collections: formData.collections, ).map(edge => edge.node)}
descriptionJson: JSON.stringify( disabled={productCreateDataLoading}
formData.description errors={maybe(() => productCreateData.productCreate.errors, [])}
), fetchCategories={searchCategory}
isPublished: formData.isPublished, fetchCollections={searchCollection}
name: formData.name, fetchProductTypes={searchProductTypes}
productType: formData.productType, header={intl.formatMessage({
publicationDate: defaultMessage: "New Product",
formData.publicationDate !== "" description: "page header"
? formData.publicationDate })}
: null, productTypes={maybe(() =>
seo: { searchProductTypesOpts.data.search.edges.map(edge => edge.node)
description: formData.seoDescription, )}
title: formData.seoTitle onBack={handleBack}
}, onSubmit={handleSubmit}
sku: formData.sku, saveButtonBarState={formTransitionState}
stockQuantity: fetchMoreCategories={{
formData.stockQuantity !== null hasMore: maybe(
? formData.stockQuantity () => searchCategoryOpts.data.search.pageInfo.hasNextPage
: 0 ),
} loading: searchCategoryOpts.loading,
}); onFetchMore: loadMoreCategories
};
const formTransitionState = getMutationState(
productCreateCalled,
productCreateDataLoading,
maybe(() => productCreateData.productCreate.errors)
);
return (
<>
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Create Product",
description: "window title"
})}
/>
<ProductCreatePage
currency={maybe(() => shop.defaultCurrency)}
categories={maybe(
() => searchCategoryOpts.data.search.edges,
[]
).map(edge => edge.node)}
collections={maybe(
() => searchCollectionOpts.data.search.edges,
[]
).map(edge => edge.node)}
disabled={productCreateDataLoading}
errors={maybe(
() => productCreateData.productCreate.errors,
[]
)}
fetchCategories={searchCategory}
fetchCollections={searchCollection}
fetchProductTypes={searchProductTypes}
header={intl.formatMessage({
defaultMessage: "New Product",
description: "page header"
})}
productTypes={maybe(() =>
searchProductTypesOpts.data.search.edges.map(
edge => edge.node
)
)}
onBack={handleBack}
onSubmit={handleSubmit}
saveButtonBarState={formTransitionState}
fetchMoreCategories={{
hasMore: maybe(
() =>
searchCategoryOpts.data.search.pageInfo
.hasNextPage
),
loading: searchCategoryOpts.loading,
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: maybe(
() =>
searchCollectionOpts.data.search.pageInfo
.hasNextPage
),
loading: searchCollectionOpts.loading,
onFetchMore: loadMoreCollections
}}
fetchMoreProductTypes={{
hasMore: maybe(
() =>
searchProductTypesOpts.data.search.pageInfo
.hasNextPage
),
loading: searchProductTypesOpts.loading,
onFetchMore: loadMoreProductTypes
}}
/>
</>
);
}}
</TypedProductCreateMutation>
);
}} }}
</SearchProductTypes> fetchMoreCollections={{
)} hasMore: maybe(
</SearchCollections> () => searchCollectionOpts.data.search.pageInfo.hasNextPage
)} ),
</SearchCategories> loading: searchCollectionOpts.loading,
onFetchMore: loadMoreCollections
}}
fetchMoreProductTypes={{
hasMore: maybe(
() => searchProductTypesOpts.data.search.pageInfo.hasNextPage
),
loading: searchProductTypesOpts.loading,
onFetchMore: loadMoreProductTypes
}}
/>
</>
);
}}
</TypedProductCreateMutation>
); );
}; };
export default ProductUpdate; export default ProductUpdate;

View file

@ -7,6 +7,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import placeholderImg from "@assets/images/placeholder255x255.png"; import placeholderImg from "@assets/images/placeholder255x255.png";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
@ -14,9 +15,8 @@ import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog"; import ProductVariantCreateDialog from "@saleor/products/components/ProductVariantCreateDialog/ProductVariantCreateDialog";
import { ProductVariantBulkCreate } from "@saleor/products/types/ProductVariantBulkCreate"; import { ProductVariantBulkCreate } from "@saleor/products/types/ProductVariantBulkCreate";
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config"; import useCategorySearch from "@saleor/searches/useCategorySearch";
import SearchCategories from "../../../containers/SearchCategories"; import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import SearchCollections from "../../../containers/SearchCollections";
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";
@ -55,6 +55,20 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
); );
const intl = useIntl(); const intl = useIntl();
const shop = useShop(); const shop = useShop();
const {
loadMore: loadMoreCategories,
search: searchCategories,
result: searchCategoriesOpts
} = useCategorySearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const {
loadMore: loadMoreCollections,
search: searchCollections,
result: searchCollectionsOpts
} = useCollectionSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const openModal = (action: ProductUrlDialog) => const openModal = (action: ProductUrlDialog) =>
navigate( navigate(
@ -64,356 +78,320 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
); );
return ( return (
<SearchCategories variables={DEFAULT_INITIAL_SEARCH_DATA}> <TypedProductDetailsQuery
{({ displayLoader
loadMore: loadMoreCategories, require={["product"]}
search: searchCategories, variables={{ id }}
result: searchCategoriesOpts >
}) => ( {({ data, loading, refetch }) => {
<SearchCollections variables={DEFAULT_INITIAL_SEARCH_DATA}> const handleDelete = () => {
{({ notify({
loadMore: loadMoreCollections, text: intl.formatMessage({
search: searchCollections, defaultMessage: "Product removed"
result: searchCollectionsOpts })
}) => ( });
<TypedProductDetailsQuery navigate(productListUrl());
displayLoader };
require={["product"]} const handleUpdate = (data: ProductUpdateMutationResult) => {
variables={{ id }} if (data.productUpdate.errors.length === 0) {
> notify({
{({ data, loading, refetch }) => { text: intl.formatMessage(commonMessages.savedChanges)
const handleDelete = () => { });
notify({ } else {
text: intl.formatMessage({ const attributeError = data.productUpdate.errors.find(
defaultMessage: "Product removed" err => err.field === "attributes"
}) );
}); if (!!attributeError) {
navigate(productListUrl()); notify({ text: attributeError.message });
}; }
const handleUpdate = (data: ProductUpdateMutationResult) => { }
if (data.productUpdate.errors.length === 0) { };
notify({
text: intl.formatMessage(commonMessages.savedChanges)
});
} else {
const attributeError = data.productUpdate.errors.find(
err => err.field === "attributes"
);
if (!!attributeError) {
notify({ text: attributeError.message });
}
}
};
const handleImageCreate = (data: ProductImageCreate) => { const handleImageCreate = (data: ProductImageCreate) => {
const imageError = data.productImageCreate.errors.find( const imageError = data.productImageCreate.errors.find(
error => error =>
error.field === error.field === ("image" as keyof ProductImageCreateVariables)
("image" as keyof ProductImageCreateVariables) );
); if (imageError) {
if (imageError) { notify({
notify({ text: imageError.message
text: imageError.message });
}); }
} };
}; const handleImageDeleteSuccess = () =>
const handleImageDeleteSuccess = () => notify({
notify({ text: intl.formatMessage(commonMessages.savedChanges)
text: intl.formatMessage(commonMessages.savedChanges) });
}); const handleVariantAdd = () => navigate(productVariantAddUrl(id));
const handleVariantAdd = () =>
navigate(productVariantAddUrl(id));
const handleBulkProductVariantCreate = ( const handleBulkProductVariantCreate = (
data: ProductVariantBulkCreate data: ProductVariantBulkCreate
) => { ) => {
if (data.productVariantBulkCreate.errors.length === 0) { if (data.productVariantBulkCreate.errors.length === 0) {
navigate(productUrl(id), true); navigate(productUrl(id), true);
refetch(); refetch();
} }
}; };
const handleBulkProductVariantDelete = ( const handleBulkProductVariantDelete = (
data: ProductVariantBulkDelete data: ProductVariantBulkDelete
) => { ) => {
if (data.productVariantBulkDelete.errors.length === 0) { if (data.productVariantBulkDelete.errors.length === 0) {
navigate(productUrl(id), true); navigate(productUrl(id), true);
reset(); reset();
refetch(); refetch();
} }
}; };
const handleVariantCreatorOpen = () => const handleVariantCreatorOpen = () =>
navigate( navigate(
productUrl(id, { productUrl(id, {
...params, ...params,
action: "create-variants" action: "create-variants"
}) })
); );
const product = data ? data.product : undefined; const product = data ? data.product : undefined;
return ( return (
<ProductUpdateOperations <ProductUpdateOperations
product={product}
onBulkProductVariantCreate={handleBulkProductVariantCreate}
onBulkProductVariantDelete={handleBulkProductVariantDelete}
onDelete={handleDelete}
onImageCreate={handleImageCreate}
onImageDelete={handleImageDeleteSuccess}
onUpdate={handleUpdate}
>
{({
bulkProductVariantCreate,
bulkProductVariantDelete,
createProductImage,
deleteProduct,
deleteProductImage,
reorderProductImages,
updateProduct,
updateSimpleProduct
}) => {
const handleImageDelete = (id: string) => () =>
deleteProductImage.mutate({ id });
const handleImageEdit = (imageId: string) => () =>
navigate(productImageUrl(id, imageId));
const handleSubmit = createUpdateHandler(
product,
updateProduct.mutate,
updateSimpleProduct.mutate
);
const handleImageUpload = createImageUploadHandler(
id,
createProductImage.mutate
);
const handleImageReorder = createImageReorderHandler(
product,
reorderProductImages.mutate
);
const disableFormSave =
createProductImage.opts.loading ||
deleteProduct.opts.loading ||
reorderProductImages.opts.loading ||
updateProduct.opts.loading ||
loading;
const formTransitionState = getMutationState(
updateProduct.opts.called || updateSimpleProduct.opts.called,
updateProduct.opts.loading || updateSimpleProduct.opts.loading,
maybe(() => updateProduct.opts.data.productUpdate.errors),
maybe(() => updateSimpleProduct.opts.data.productUpdate.errors),
maybe(
() =>
updateSimpleProduct.opts.data.productVariantUpdate.errors
)
);
const deleteTransitionState = getMutationState(
deleteProduct.opts.called,
deleteProduct.opts.loading,
maybe(() => deleteProduct.opts.data.productDelete.errors)
);
const bulkProductVariantDeleteTransitionState = getMutationState(
bulkProductVariantDelete.opts.called,
bulkProductVariantDelete.opts.loading,
maybe(
() =>
bulkProductVariantDelete.opts.data.productVariantBulkDelete
.errors
)
);
const categories = maybe(
() => searchCategoriesOpts.data.search.edges,
[]
).map(edge => edge.node);
const collections = maybe(
() => searchCollectionsOpts.data.search.edges,
[]
).map(edge => edge.node);
const errors = maybe(
() => updateProduct.opts.data.productUpdate.errors,
[]
);
return (
<>
<WindowTitle title={maybe(() => data.product.name)} />
<ProductUpdatePage
categories={categories}
collections={collections}
disabled={disableFormSave}
errors={errors}
fetchCategories={searchCategories}
fetchCollections={searchCollections}
saveButtonBarState={formTransitionState}
images={maybe(() => data.product.images)}
header={maybe(() => product.name)}
placeholderImage={placeholderImg}
product={product} product={product}
onBulkProductVariantCreate={handleBulkProductVariantCreate} variants={maybe(() => product.variants)}
onBulkProductVariantDelete={handleBulkProductVariantDelete} onBack={() => {
onDelete={handleDelete} navigate(productListUrl());
onImageCreate={handleImageCreate}
onImageDelete={handleImageDeleteSuccess}
onUpdate={handleUpdate}
>
{({
bulkProductVariantCreate,
bulkProductVariantDelete,
createProductImage,
deleteProduct,
deleteProductImage,
reorderProductImages,
updateProduct,
updateSimpleProduct
}) => {
const handleImageDelete = (id: string) => () =>
deleteProductImage.mutate({ id });
const handleImageEdit = (imageId: string) => () =>
navigate(productImageUrl(id, imageId));
const handleSubmit = createUpdateHandler(
product,
updateProduct.mutate,
updateSimpleProduct.mutate
);
const handleImageUpload = createImageUploadHandler(
id,
createProductImage.mutate
);
const handleImageReorder = createImageReorderHandler(
product,
reorderProductImages.mutate
);
const disableFormSave =
createProductImage.opts.loading ||
deleteProduct.opts.loading ||
reorderProductImages.opts.loading ||
updateProduct.opts.loading ||
loading;
const formTransitionState = getMutationState(
updateProduct.opts.called ||
updateSimpleProduct.opts.called,
updateProduct.opts.loading ||
updateSimpleProduct.opts.loading,
maybe(
() => updateProduct.opts.data.productUpdate.errors
),
maybe(
() =>
updateSimpleProduct.opts.data.productUpdate.errors
),
maybe(
() =>
updateSimpleProduct.opts.data.productVariantUpdate
.errors
)
);
const deleteTransitionState = getMutationState(
deleteProduct.opts.called,
deleteProduct.opts.loading,
maybe(
() => deleteProduct.opts.data.productDelete.errors
)
);
const bulkProductVariantDeleteTransitionState = getMutationState(
bulkProductVariantDelete.opts.called,
bulkProductVariantDelete.opts.loading,
maybe(
() =>
bulkProductVariantDelete.opts.data
.productVariantBulkDelete.errors
)
);
const categories = maybe(
() => searchCategoriesOpts.data.search.edges,
[]
).map(edge => edge.node);
const collections = maybe(
() => searchCollectionsOpts.data.search.edges,
[]
).map(edge => edge.node);
const errors = maybe(
() => updateProduct.opts.data.productUpdate.errors,
[]
);
return (
<>
<WindowTitle title={maybe(() => data.product.name)} />
<ProductUpdatePage
categories={categories}
collections={collections}
disabled={disableFormSave}
errors={errors}
fetchCategories={searchCategories}
fetchCollections={searchCollections}
saveButtonBarState={formTransitionState}
images={maybe(() => data.product.images)}
header={maybe(() => product.name)}
placeholderImage={placeholderImg}
product={product}
variants={maybe(() => product.variants)}
onBack={() => {
navigate(productListUrl());
}}
onDelete={() => openModal("remove")}
onProductShow={() => {
if (product) {
window.open(product.url);
}
}}
onImageReorder={handleImageReorder}
onSubmit={handleSubmit}
onVariantAdd={handleVariantAdd}
onVariantsAdd={handleVariantCreatorOpen}
onVariantShow={variantId => () =>
navigate(
productVariantEditUrl(product.id, variantId)
)}
onImageUpload={handleImageUpload}
onImageEdit={handleImageEdit}
onImageDelete={handleImageDelete}
toolbar={
<IconButton
color="primary"
onClick={() =>
navigate(
productUrl(id, {
action: "remove-variants",
ids: listElements
})
)
}
>
<DeleteIcon />
</IconButton>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
fetchMoreCategories={{
hasMore: maybe(
() =>
searchCategoriesOpts.data.search.pageInfo
.hasNextPage
),
loading: searchCategoriesOpts.loading,
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: maybe(
() =>
searchCollectionsOpts.data.search.pageInfo
.hasNextPage
),
loading: searchCollectionsOpts.loading,
onFetchMore: loadMoreCollections
}}
/>
<ActionDialog
open={params.action === "remove"}
onClose={() => navigate(productUrl(id), true)}
confirmButtonState={deleteTransitionState}
onConfirm={() => deleteProduct.mutate({ id })}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete Product",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {name}?"
description="delete product"
values={{
name: product ? product.name : undefined
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "remove-variants"}
onClose={() => navigate(productUrl(id), true)}
confirmButtonState={
bulkProductVariantDeleteTransitionState
}
onConfirm={() =>
bulkProductVariantDelete.mutate({
ids: params.ids
})
}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete Product Variants",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {counter,plural,one{this variant} other{{displayQuantity} variants}}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ProductVariantCreateDialog
defaultPrice={maybe(() =>
data.product.basePrice.amount.toFixed(2)
)}
errors={maybe(
() =>
bulkProductVariantCreate.opts.data
.productVariantBulkCreate.bulkProductErrors,
[]
)}
open={params.action === "create-variants"}
attributes={maybe(
() => data.product.productType.variantAttributes,
[]
)}
currencySymbol={maybe(() => shop.defaultCurrency)}
onClose={() =>
navigate(
productUrl(id, {
...params,
action: undefined
})
)
}
onSubmit={inputs =>
bulkProductVariantCreate.mutate({
id,
inputs
})
}
/>
</>
);
}} }}
</ProductUpdateOperations> onDelete={() => openModal("remove")}
); onProductShow={() => {
}} if (product) {
</TypedProductDetailsQuery> window.open(product.url);
)} }
</SearchCollections> }}
)} onImageReorder={handleImageReorder}
</SearchCategories> onSubmit={handleSubmit}
onVariantAdd={handleVariantAdd}
onVariantsAdd={handleVariantCreatorOpen}
onVariantShow={variantId => () =>
navigate(productVariantEditUrl(product.id, variantId))}
onImageUpload={handleImageUpload}
onImageEdit={handleImageEdit}
onImageDelete={handleImageDelete}
toolbar={
<IconButton
color="primary"
onClick={() =>
navigate(
productUrl(id, {
action: "remove-variants",
ids: listElements
})
)
}
>
<DeleteIcon />
</IconButton>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
fetchMoreCategories={{
hasMore: maybe(
() =>
searchCategoriesOpts.data.search.pageInfo.hasNextPage
),
loading: searchCategoriesOpts.loading,
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: maybe(
() =>
searchCollectionsOpts.data.search.pageInfo.hasNextPage
),
loading: searchCollectionsOpts.loading,
onFetchMore: loadMoreCollections
}}
/>
<ActionDialog
open={params.action === "remove"}
onClose={() => navigate(productUrl(id), true)}
confirmButtonState={deleteTransitionState}
onConfirm={() => deleteProduct.mutate({ id })}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete Product",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {name}?"
description="delete product"
values={{
name: product ? product.name : undefined
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "remove-variants"}
onClose={() => navigate(productUrl(id), true)}
confirmButtonState={bulkProductVariantDeleteTransitionState}
onConfirm={() =>
bulkProductVariantDelete.mutate({
ids: params.ids
})
}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete Product Variants",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {counter,plural,one{this variant} other{{displayQuantity} variants}}?"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ProductVariantCreateDialog
defaultPrice={maybe(() =>
data.product.basePrice.amount.toFixed(2)
)}
errors={maybe(
() =>
bulkProductVariantCreate.opts.data
.productVariantBulkCreate.bulkProductErrors,
[]
)}
open={params.action === "create-variants"}
attributes={maybe(
() => data.product.productType.variantAttributes,
[]
)}
currencySymbol={maybe(() => shop.defaultCurrency)}
onClose={() =>
navigate(
productUrl(id, {
...params,
action: undefined
})
)
}
onSubmit={inputs =>
bulkProductVariantCreate.mutate({
id,
inputs
})
}
/>
</>
);
}}
</ProductUpdateOperations>
);
}}
</TypedProductDetailsQuery>
); );
}; };
export default ProductUpdate; export default ProductUpdate;

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // This file was automatically generated and should not be edited.
import { AttributeInputTypeEnum } from "./../../../types/globalTypes"; import { AttributeInputTypeEnum } from "./../../types/globalTypes";
// ==================================================== // ====================================================
// GraphQL query operation: SearchProductTypes // GraphQL query operation: SearchProductTypes
@ -22,7 +22,9 @@ export interface SearchProductTypes_search_edges_node_productAttributes {
slug: string | null; slug: string | null;
name: string | null; name: string | null;
valueRequired: boolean; valueRequired: boolean;
values: (SearchProductTypes_search_edges_node_productAttributes_values | null)[] | null; values:
| (SearchProductTypes_search_edges_node_productAttributes_values | null)[]
| null;
} }
export interface SearchProductTypes_search_edges_node { export interface SearchProductTypes_search_edges_node {
@ -30,7 +32,9 @@ export interface SearchProductTypes_search_edges_node {
id: string; id: string;
name: string; name: string;
hasVariants: boolean; hasVariants: boolean;
productAttributes: (SearchProductTypes_search_edges_node_productAttributes | null)[] | null; productAttributes:
| (SearchProductTypes_search_edges_node_productAttributes | null)[]
| null;
} }
export interface SearchProductTypes_search_edges { export interface SearchProductTypes_search_edges {

View file

@ -1,7 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { pageInfoFragment } from "@saleor/queries"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchCategories, SearchCategories,
SearchCategoriesVariables SearchCategoriesVariables
@ -24,6 +24,6 @@ export const searchCategories = gql`
} }
`; `;
export default TopLevelSearch<SearchCategories, SearchCategoriesVariables>( export default makeTopLevelSearch<SearchCategories, SearchCategoriesVariables>(
searchCategories searchCategories
); );

View file

@ -1,7 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { pageInfoFragment } from "@saleor/queries"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchCollections, SearchCollections,
SearchCollectionsVariables SearchCollectionsVariables
@ -24,6 +24,7 @@ export const searchCollections = gql`
} }
`; `;
export default TopLevelSearch<SearchCollections, SearchCollectionsVariables>( export default makeTopLevelSearch<
searchCollections SearchCollections,
); SearchCollectionsVariables
>(searchCollections);

View file

@ -1,7 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { pageInfoFragment } from "@saleor/queries"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchCustomers, SearchCustomers,
SearchCustomersVariables SearchCustomersVariables
@ -24,6 +24,6 @@ export const searchCustomers = gql`
} }
`; `;
export default TopLevelSearch<SearchCustomers, SearchCustomersVariables>( export default makeTopLevelSearch<SearchCustomers, SearchCustomersVariables>(
searchCustomers searchCustomers
); );

View file

@ -1,7 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { pageInfoFragment } from "@saleor/queries"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { SearchPages, SearchPagesVariables } from "./types/SearchPages"; import { SearchPages, SearchPagesVariables } from "./types/SearchPages";
export const searchPages = gql` export const searchPages = gql`
@ -21,4 +21,6 @@ export const searchPages = gql`
} }
`; `;
export default TopLevelSearch<SearchPages, SearchPagesVariables>(searchPages); export default makeTopLevelSearch<SearchPages, SearchPagesVariables>(
searchPages
);

View file

@ -1,7 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { pageInfoFragment } from "@saleor/queries"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchProducts, SearchProducts,
SearchProductsVariables SearchProductsVariables
@ -27,6 +27,6 @@ export const searchProducts = gql`
} }
`; `;
export default TopLevelSearch<SearchProducts, SearchProductsVariables>( export default makeTopLevelSearch<SearchProducts, SearchProductsVariables>(
searchProducts searchProducts
); );

View file

@ -1,7 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { pageInfoFragment } from "@saleor/queries"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchProductTypes, SearchProductTypes,
SearchProductTypesVariables SearchProductTypesVariables
@ -41,6 +41,7 @@ export const searchProductTypes = gql`
} }
`; `;
export default TopLevelSearch<SearchProductTypes, SearchProductTypesVariables>( export default makeTopLevelSearch<
searchProductTypes SearchProductTypes,
); SearchProductTypesVariables
>(searchProductTypes);

View file

@ -1,7 +1,7 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import makeTopLevelSearch from "@saleor/hooks/makeTopLevelSearch";
import { pageInfoFragment } from "@saleor/queries"; import { pageInfoFragment } from "@saleor/queries";
import TopLevelSearch from "../TopLevelSearch";
import { import {
SearchServiceAccount, SearchServiceAccount,
SearchServiceAccountVariables SearchServiceAccountVariables
@ -28,7 +28,7 @@ export const searchServiceAccount = gql`
} }
`; `;
export default TopLevelSearch< export default makeTopLevelSearch<
SearchServiceAccount, SearchServiceAccount,
SearchServiceAccountVariables SearchServiceAccountVariables
>(searchServiceAccount); >(searchServiceAccount);

View file

@ -125505,6 +125505,7 @@ exports[`Storyshots Views / Webhooks / Create webhook default 1`] = `
class="MuiInputBase-input-id MuiOutlinedInput-input-id" class="MuiInputBase-input-id MuiOutlinedInput-input-id"
name="name" name="name"
type="text" type="text"
value=""
/> />
<fieldset <fieldset
aria-hidden="true" aria-hidden="true"
@ -126037,6 +126038,7 @@ exports[`Storyshots Views / Webhooks / Create webhook form errors 1`] = `
class="MuiInputBase-input-id MuiOutlinedInput-input-id" class="MuiInputBase-input-id MuiOutlinedInput-input-id"
name="name" name="name"
type="text" type="text"
value=""
/> />
<fieldset <fieldset
aria-hidden="true" aria-hidden="true"
@ -126575,6 +126577,7 @@ exports[`Storyshots Views / Webhooks / Create webhook loading 1`] = `
disabled="" disabled=""
name="name" name="name"
type="text" type="text"
value=""
/> />
<fieldset <fieldset
aria-hidden="true" aria-hidden="true"

View file

@ -6,9 +6,9 @@ import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { SearchServiceAccount_search_edges_node } from "@saleor/containers/SearchServiceAccount/types/SearchServiceAccount";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { SearchServiceAccount_search_edges_node } from "@saleor/searches/types/SearchServiceAccount";
import { WebhookEventTypeEnum } from "@saleor/types/globalTypes"; import { WebhookEventTypeEnum } from "@saleor/types/globalTypes";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import WebhookEvents from "@saleor/webhooks/components/WebhookEvents"; import WebhookEvents from "@saleor/webhooks/components/WebhookEvents";
@ -19,7 +19,6 @@ import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
export interface FormData { export interface FormData {
id: string;
events: WebhookEventTypeEnum[]; events: WebhookEventTypeEnum[];
isActive: boolean; isActive: boolean;
name: string; name: string;
@ -52,9 +51,8 @@ const WebhookCreatePage: React.FC<WebhookCreatePageProps> = ({
const initialForm: FormData = { const initialForm: FormData = {
allEvents: false, allEvents: false,
events: [], events: [],
id: null,
isActive: false, isActive: false,
name: null, name: "",
secretKey: "", secretKey: "",
serviceAccount: "", serviceAccount: "",
targetUrl: "" targetUrl: ""

View file

@ -1,3 +1,6 @@
import React from "react";
import { useIntl } from "react-intl";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
@ -6,10 +9,10 @@ import FormSpacer from "@saleor/components/FormSpacer";
import Grid from "@saleor/components/Grid"; import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SaveButtonBar from "@saleor/components/SaveButtonBar";
import { SearchServiceAccount_search_edges_node } from "@saleor/containers/SearchServiceAccount/types/SearchServiceAccount";
import useStateFromProps from "@saleor/hooks/useStateFromProps"; import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { sectionNames } from "@saleor/intl"; import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { SearchServiceAccount_search_edges_node } from "@saleor/searches/types/SearchServiceAccount";
import { WebhookEventTypeEnum } from "@saleor/types/globalTypes"; import { WebhookEventTypeEnum } from "@saleor/types/globalTypes";
import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler";
import WebhookEvents from "@saleor/webhooks/components/WebhookEvents"; import WebhookEvents from "@saleor/webhooks/components/WebhookEvents";
@ -18,11 +21,7 @@ import WebhookStatus from "@saleor/webhooks/components/WebhookStatus";
import { WebhookCreate_webhookCreate_webhookErrors } from "@saleor/webhooks/types/WebhookCreate"; import { WebhookCreate_webhookCreate_webhookErrors } from "@saleor/webhooks/types/WebhookCreate";
import { WebhookDetails_webhook } from "@saleor/webhooks/types/WebhookDetails"; import { WebhookDetails_webhook } from "@saleor/webhooks/types/WebhookDetails";
import React from "react";
import { useIntl } from "react-intl";
export interface FormData { export interface FormData {
id: string;
events: WebhookEventTypeEnum[]; events: WebhookEventTypeEnum[];
isActive: boolean; isActive: boolean;
name: string; name: string;
@ -63,7 +62,6 @@ const WebhooksDetailsPage: React.FC<WebhooksDetailsPageProps> = ({
events: maybe(() => webhook.events, []) events: maybe(() => webhook.events, [])
.map(event => event.eventType) .map(event => event.eventType)
.filter(event => event !== WebhookEventTypeEnum.ANY_EVENTS), .filter(event => event !== WebhookEventTypeEnum.ANY_EVENTS),
id: maybe(() => webhook.id, null),
isActive: maybe(() => webhook.isActive, false), isActive: maybe(() => webhook.isActive, false),
name: maybe(() => webhook.name, ""), name: maybe(() => webhook.name, ""),
secretKey: maybe(() => webhook.secretKey, ""), secretKey: maybe(() => webhook.secretKey, ""),

View file

@ -1,13 +1,13 @@
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import SearchServiceAccount from "@saleor/containers/SearchServiceAccount"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
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 useServiceAccountSearch from "@saleor/searches/useServiceAccountSearch";
import { WebhookEventTypeEnum } from "@saleor/types/globalTypes"; import { WebhookEventTypeEnum } from "@saleor/types/globalTypes";
import { WebhookCreate as WebhookCreateData } from "@saleor/webhooks/types/WebhookCreate"; import { WebhookCreate as WebhookCreateData } from "@saleor/webhooks/types/WebhookCreate";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../config";
import { getMutationState, maybe } from "../../misc"; import { getMutationState, maybe } from "../../misc";
import WebhookCreatePage, { FormData } from "../components/WebhookCreatePage"; import WebhookCreatePage, { FormData } from "../components/WebhookCreatePage";
import { TypedWebhookCreate } from "../mutations"; import { TypedWebhookCreate } from "../mutations";
@ -26,6 +26,12 @@ export const WebhooksCreate: React.FC<WebhooksCreateProps> = () => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
const {
search: searchServiceAccount,
result: searchServiceAccountOpt
} = useServiceAccountSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const onSubmit = (data: WebhookCreateData) => { const onSubmit = (data: WebhookCreateData) => {
if (data.webhookCreate.webhookErrors.length === 0) { if (data.webhookCreate.webhookErrors.length === 0) {
@ -39,62 +45,56 @@ export const WebhooksCreate: React.FC<WebhooksCreateProps> = () => {
const handleBack = () => navigate(webhooksListUrl()); const handleBack = () => navigate(webhooksListUrl());
return ( return (
<SearchServiceAccount variables={DEFAULT_INITIAL_SEARCH_DATA}> <TypedWebhookCreate onCompleted={onSubmit}>
{({ search: searchServiceAccount, result: searchServiceAccountOpt }) => ( {(webhookCreate, webhookCreateOpts) => {
<TypedWebhookCreate onCompleted={onSubmit}> const handleSubmit = (data: FormData) =>
{(webhookCreate, webhookCreateOpts) => { webhookCreate({
const handleSubmit = (data: FormData) => variables: {
webhookCreate({ input: {
variables: { events: data.allEvents
input: { ? [WebhookEventTypeEnum.ANY_EVENTS]
events: data.allEvents : data.events,
? [WebhookEventTypeEnum.ANY_EVENTS] isActive: data.isActive,
: data.events, name: data.name,
isActive: data.isActive, secretKey: data.secretKey,
name: data.name, serviceAccount: data.serviceAccount,
secretKey: data.secretKey, targetUrl: data.targetUrl
serviceAccount: data.serviceAccount, }
targetUrl: data.targetUrl }
} });
}
});
const formTransitionState = getMutationState( const formTransitionState = getMutationState(
webhookCreateOpts.called, webhookCreateOpts.called,
webhookCreateOpts.loading, webhookCreateOpts.loading,
maybe(() => webhookCreateOpts.data.webhookCreate.webhookErrors) maybe(() => webhookCreateOpts.data.webhookCreate.webhookErrors)
); );
return ( return (
<> <>
<WindowTitle <WindowTitle
title={intl.formatMessage({ title={intl.formatMessage({
defaultMessage: "Create Webhook", defaultMessage: "Create Webhook",
description: "window title" description: "window title"
})} })}
/> />
<WebhookCreatePage <WebhookCreatePage
disabled={false} disabled={false}
errors={maybe( errors={maybe(
() => webhookCreateOpts.data.webhookCreate.webhookErrors, () => webhookCreateOpts.data.webhookCreate.webhookErrors,
[] []
)} )}
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={handleBack}
)} onSubmit={handleSubmit}
onBack={handleBack} saveButtonBarState={formTransitionState}
onSubmit={handleSubmit} />
saveButtonBarState={formTransitionState} </>
/> );
</> }}
); </TypedWebhookCreate>
}}
</TypedWebhookCreate>
)}
</SearchServiceAccount>
); );
}; };
WebhooksCreate.displayName = "WebhooksCreate"; WebhooksCreate.displayName = "WebhooksCreate";

View file

@ -1,15 +1,16 @@
import React from "react";
import { useIntl } from "react-intl";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import SearchServiceAccount from "@saleor/containers/SearchServiceAccount"; import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
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 useServiceAccountSearch from "@saleor/searches/useServiceAccountSearch";
import { WebhookEventTypeEnum } from "@saleor/types/globalTypes"; import { WebhookEventTypeEnum } from "@saleor/types/globalTypes";
import WebhookDeleteDialog from "@saleor/webhooks/components/WebhookDeleteDialog"; 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 React from "react";
import { useIntl } from "react-intl";
import { DEFAULT_INITIAL_SEARCH_DATA } from "../../config";
import { getMutationState, maybe } from "../../misc"; import { getMutationState, maybe } from "../../misc";
import WebhooksDetailsPage from "../components/WebhooksDetailsPage"; import WebhooksDetailsPage from "../components/WebhooksDetailsPage";
import { TypedWebhookDelete, TypedWebhookUpdate } from "../mutations"; import { TypedWebhookDelete, TypedWebhookUpdate } from "../mutations";
@ -33,6 +34,12 @@ export const WebhooksDetails: React.FC<WebhooksDetailsProps> = ({
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
const intl = useIntl(); const intl = useIntl();
const {
search: searchServiceAccount,
result: searchServiceAccountOpt
} = useServiceAccountSearch({
variables: DEFAULT_INITIAL_SEARCH_DATA
});
const closeModal = () => const closeModal = () =>
navigate( navigate(
@ -72,96 +79,92 @@ export const WebhooksDetails: React.FC<WebhooksDetailsProps> = ({
}; };
return ( return (
<SearchServiceAccount variables={DEFAULT_INITIAL_SEARCH_DATA}> <TypedWebhookUpdate onCompleted={onWebhookUpdate}>
{({ search: searchServiceAccount, result: searchServiceAccountOpt }) => ( {(webhookUpdate, webhookUpdateOpts) => (
<TypedWebhookUpdate onCompleted={onWebhookUpdate}> <TypedWebhookDelete onCompleted={onWebhookDelete}>
{(webhookUpdate, webhookUpdateOpts) => ( {(webhookDelete, webhookDeleteOpts) => (
<TypedWebhookDelete onCompleted={onWebhookDelete}> <TypedWebhooksDetailsQuery variables={{ id }}>
{(webhookDelete, webhookDeleteOpts) => ( {webhookDetails => {
<TypedWebhooksDetailsQuery variables={{ id }}> const formTransitionState = getMutationState(
{webhookDetails => { webhookUpdateOpts.called,
const formTransitionState = getMutationState( webhookUpdateOpts.loading,
webhookUpdateOpts.called, maybe(
webhookUpdateOpts.loading, () => webhookUpdateOpts.data.webhookUpdate.webhookErrors
maybe( )
() => webhookUpdateOpts.data.webhookUpdate.webhookErrors );
)
);
const handleRemoveConfirm = () => const handleRemoveConfirm = () =>
webhookDelete({ webhookDelete({
variables: { variables: {
id id
} }
}); });
const formErrors = maybe( const formErrors = maybe(
() => webhookUpdateOpts.data.webhookUpdate.webhookErrors, () => webhookUpdateOpts.data.webhookUpdate.webhookErrors,
[] []
); );
const deleteTransitionState = getMutationState( const deleteTransitionState = getMutationState(
webhookDeleteOpts.called, webhookDeleteOpts.called,
webhookDeleteOpts.loading, webhookDeleteOpts.loading,
maybe(() => webhookDeleteOpts.data.webhookDelete.errors) maybe(() => webhookDeleteOpts.data.webhookDelete.errors)
); );
return ( return (
<> <>
<WindowTitle <WindowTitle
title={maybe(() => webhookDetails.data.webhook.name)} title={maybe(() => webhookDetails.data.webhook.name)}
/> />
<WebhooksDetailsPage <WebhooksDetailsPage
disabled={webhookDetails.loading} disabled={webhookDetails.loading}
errors={formErrors} errors={formErrors}
saveButtonBarState={formTransitionState} saveButtonBarState={formTransitionState}
webhook={maybe(() => webhookDetails.data.webhook)} webhook={maybe(() => webhookDetails.data.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(webhooksListUrl())} onBack={() => navigate(webhooksListUrl())}
onDelete={() => openModal("remove")} onDelete={() => openModal("remove")}
onSubmit={data => { onSubmit={data => {
webhookUpdate({ webhookUpdate({
variables: { variables: {
id, id,
input: { input: {
events: data.allEvents events: data.allEvents
? [WebhookEventTypeEnum.ANY_EVENTS] ? [WebhookEventTypeEnum.ANY_EVENTS]
: data.events, : data.events,
isActive: data.isActive, isActive: data.isActive,
name: data.name, name: data.name,
secretKey: data.secretKey, secretKey: data.secretKey,
serviceAccount: data.serviceAccount, serviceAccount: data.serviceAccount,
targetUrl: data.targetUrl targetUrl: data.targetUrl
} }
} }
}); });
}} }}
/> />
<WebhookDeleteDialog <WebhookDeleteDialog
confirmButtonState={deleteTransitionState} confirmButtonState={deleteTransitionState}
name={maybe( name={maybe(
() => webhookDetails.data.webhook.name, () => webhookDetails.data.webhook.name,
"..." "..."
)} )}
onClose={closeModal} onClose={closeModal}
onConfirm={handleRemoveConfirm} onConfirm={handleRemoveConfirm}
open={params.action === "remove"} open={params.action === "remove"}
/> />
</> </>
); );
}} }}
</TypedWebhooksDetailsQuery> </TypedWebhooksDetailsQuery>
)}
</TypedWebhookDelete>
)} )}
</TypedWebhookUpdate> </TypedWebhookDelete>
)} )}
</SearchServiceAccount> </TypedWebhookUpdate>
); );
}; };
WebhooksDetails.displayName = "WebhooksDetails"; WebhooksDetails.displayName = "WebhooksDetails";