From bd211a6720b86990610110c51f848b8b30d63a30 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Fri, 9 Aug 2019 13:14:35 +0200 Subject: [PATCH] Update from saleor/master --- src/auth/components/LoginPage/LoginPage.tsx | 8 +- src/auth/mutations.ts | 3 + src/auth/types/TokenAuth.ts | 6 + src/auth/types/User.ts | 6 + src/auth/types/VerifyToken.ts | 6 + .../components/CategoryList/CategoryList.tsx | 18 +- .../CategoryListPage/CategoryListPage.tsx | 8 +- .../CategoryProductsCard.tsx | 4 + src/categories/views/CategoryList.tsx | 15 +- .../CollectionList/CollectionList.tsx | 18 +- .../CollectionListPage/CollectionListPage.tsx | 4 +- .../CollectionProducts/CollectionProducts.tsx | 58 +- src/collections/fixtures.ts | 40 + src/collections/views/CollectionDetails.tsx | 8 +- src/collections/views/CollectionList.tsx | 14 +- src/components/ActionDialog/ActionDialog.tsx | 2 +- src/components/AddressEdit/AddressEdit.tsx | 30 +- src/components/AppLayout/AppLayout.tsx | 168 +- src/components/AppLayout/MenuList.tsx | 350 +- src/components/AppLayout/MenuNested.tsx | 230 +- src/components/AppLayout/ResponsiveDrawer.tsx | 19 +- src/components/AppLayout/consts.ts | 3 +- src/components/AppLayout/menuStructure.ts | 14 + .../AssignCategoryDialog.tsx | 210 +- .../AssignCollectionDialog.tsx | 213 +- .../AssignProductDialog.tsx | 226 +- src/components/CardTitle/CardTitle.tsx | 4 +- src/components/Checkbox/Checkbox.tsx | 19 +- src/components/Container.tsx | 2 +- src/components/ControlledCheckbox.tsx | 2 +- src/components/ControlledSwitch.tsx | 17 +- src/components/Date/Date.test.tsx | 2 +- src/components/Date/Date.tsx | 2 +- src/components/Date/DateTime.tsx | 2 +- src/components/DraftRenderer.tsx | 2 +- src/components/Dropzone.tsx | 2 +- .../EditableTableCell/EditableTableCell.tsx | 63 +- src/components/ErrorPage/ErrorPage.tsx | 2 +- .../ExtendedPageHeader/ExtendedPageHeader.tsx | 2 +- src/components/Filter/Filter.tsx | 2 +- src/components/Filter/FilterContent.tsx | 2 +- src/components/Filter/FilterElement.tsx | 2 +- src/components/FilterBar/FilterBar.tsx | 1 + src/components/Form/Form.tsx | 170 +- src/components/Form/index.ts | 3 +- src/components/Link.tsx | 2 +- .../MultiAutocompleteSelectField.tsx | 360 +- src/components/NotFoundPage/NotFoundPage.tsx | 4 +- src/components/PageHeader/PageHeader.tsx | 5 +- src/components/ProductList/ProductList.tsx | 285 +- .../RichTextEditor/RichTextEditor.tsx | 107 +- .../SaveButtonBar/SaveButtonBar.tsx | 4 +- src/components/SeoForm/SeoForm.tsx | 7 +- .../SingleAutocompleteSelectField.tsx | 144 +- src/components/Skeleton.tsx | 15 +- src/components/StatusLabel/StatusLabel.tsx | 2 +- src/components/Tab/Tab.tsx | 2 +- .../TableCellAvatar/TableCellAvatar.tsx | 53 +- src/components/TableFilter/FilterChips.tsx | 2 +- src/components/TableHead/TableHead.tsx | 44 +- .../TablePagination/TablePagination.tsx | 18 +- .../TablePaginationActions.tsx | 2 +- .../TextFieldWithChoice.tsx | 2 +- src/components/Theme/ThemeProvider.tsx | 22 +- src/components/Timeline/TimelineNote.tsx | 4 +- src/config.ts | 56 + src/configuration/index.tsx | 44 +- .../CustomerAddressDialog.tsx | 100 +- .../CustomerCreateAddress.tsx | 17 +- .../CustomerCreatePage/CustomerCreatePage.tsx | 109 +- .../CustomerDetails/CustomerDetails.tsx | 2 +- .../CustomerDetailsPage.tsx | 8 +- .../components/CustomerList/CustomerList.tsx | 18 +- .../CustomerListPage/CustomerListPage.tsx | 4 +- src/customers/types.ts | 5 +- src/customers/views/CustomerAddresses.tsx | 35 +- src/customers/views/CustomerCreate.tsx | 10 +- src/customers/views/CustomerList.tsx | 14 +- .../DiscountCategories/DiscountCategories.tsx | 14 +- .../DiscountCollections.tsx | 14 +- .../DiscountProducts/DiscountProducts.tsx | 62 +- .../components/SaleList/SaleList.tsx | 22 +- .../components/SaleListPage/SaleListPage.tsx | 4 +- .../components/SalePricing/SalePricing.tsx | 6 + .../VoucherCreatePage/VoucherCreatePage.tsx | 79 +- .../VoucherDetailsPage/VoucherDetailsPage.tsx | 117 +- .../components/VoucherInfo/VoucherInfo.tsx | 128 +- .../components/VoucherList/VoucherList.tsx | 28 +- .../VoucherListPage/VoucherListPage.tsx | 8 +- .../VoucherSummary/VoucherSummary.tsx | 8 +- src/discounts/fixtures.ts | 21 +- src/discounts/queries.ts | 4 +- src/discounts/translations.ts | 10 +- src/discounts/types/VoucherCataloguesAdd.ts | 11 +- .../types/VoucherCataloguesRemove.ts | 11 +- src/discounts/types/VoucherCreate.ts | 7 +- src/discounts/types/VoucherDetails.ts | 11 +- src/discounts/types/VoucherDetailsFragment.ts | 11 +- src/discounts/types/VoucherFragment.ts | 7 +- src/discounts/types/VoucherList.ts | 7 +- src/discounts/types/VoucherUpdate.ts | 7 +- src/discounts/views/SaleDetails.tsx | 12 +- src/discounts/views/SaleList.tsx | 14 +- src/discounts/views/VoucherCreate.tsx | 51 +- src/discounts/views/VoucherDetails.tsx | 87 +- src/discounts/views/VoucherList.tsx | 14 +- src/fixtures.ts | 20 +- src/home/components/HomeHeader/HomeHeader.tsx | 38 +- .../HomeProductListCard.tsx | 8 +- src/home/queries.ts | 1 - src/home/types/Home.ts | 3 +- src/hooks/useBulkActions.ts | 34 +- src/icons/Calendar.tsx | 2 +- src/icons/Draggable.tsx | 2 +- src/icons/Image.tsx | 2 +- src/icons/Navigation.tsx | 4 +- src/icons/Orders.tsx | 2 +- src/icons/Pages.tsx | 2 +- src/icons/ProductTypes.tsx | 2 +- src/icons/Sales.tsx | 2 +- src/icons/ShippingMethods.tsx | 2 +- src/icons/SiteSettings.tsx | 2 +- src/icons/StaffMembers.tsx | 2 +- src/icons/Taxes.tsx | 2 +- src/index.tsx | 257 +- src/misc.ts | 47 +- .../MenuItemDialog/MenuItemDialog.tsx | 176 +- .../components/MenuList/MenuList.tsx | 21 +- .../components/MenuListPage/MenuListPage.tsx | 4 +- src/navigation/urls.ts | 1 + src/navigation/views/MenuDetails/index.tsx | 21 +- src/navigation/views/MenuDetails/utils.ts | 40 +- src/navigation/views/MenuList.tsx | 16 +- .../OrderAddressEditDialog.tsx | 103 +- .../OrderCustomer/OrderCustomer.tsx | 36 +- .../OrderCustomerEditDialog.tsx | 10 +- .../OrderDraftDetailsProducts.tsx | 60 +- .../OrderDraftList/OrderDraftList.tsx | 20 +- .../OrderDraftListPage/OrderDraftListPage.tsx | 4 +- .../OrderFulfillment/OrderFulfillment.tsx | 63 +- .../OrderFulfillmentDialog.tsx | 96 +- src/orders/components/OrderList/OrderList.tsx | 20 +- .../OrderListFilter/OrderListFilter.tsx | 2 +- .../OrderPaymentDialog/OrderPaymentDialog.tsx | 4 +- .../OrderProductAddDialog.tsx | 447 +- .../OrderUnfulfilledItems.tsx | 57 +- src/orders/views/OrderDetails/index.tsx | 26 +- src/orders/views/OrderDraftList.tsx | 14 +- src/orders/views/OrderList/OrderList.tsx | 12 +- src/pages/components/PageList/PageList.tsx | 21 +- .../components/PageListPage/PageListPage.tsx | 8 +- src/pages/views/PageList.tsx | 17 +- .../ProductTypeAttributes.tsx | 162 +- .../ProductTypeCreatePage.tsx | 70 +- .../ProductTypeDetails/ProductTypeDetails.tsx | 8 +- .../ProductTypeDetailsPage.tsx | 105 +- .../ProductTypeList/ProductTypeList.tsx | 14 +- .../ProductTypeTaxes/ProductTypeTaxes.tsx | 76 +- .../containers/ProductTypeOperations.tsx | 198 +- src/productTypes/fixtures.ts | 128 +- src/productTypes/mutations.ts | 120 +- src/productTypes/queries.ts | 34 +- src/productTypes/types/ProductTypeCreate.ts | 28 +- .../types/ProductTypeCreateData.ts | 7 + src/productTypes/types/ProductTypeDetails.ts | 35 +- .../types/ProductTypeDetailsFragment.ts | 28 +- src/productTypes/types/ProductTypeFragment.ts | 10 +- src/productTypes/types/ProductTypeList.ts | 12 +- src/productTypes/types/ProductTypeUpdate.ts | 28 +- src/productTypes/urls.ts | 11 +- src/productTypes/views/ProductTypeCreate.tsx | 12 +- .../views/ProductTypeUpdate/index.tsx | 475 +- .../ProductCreatePage/ProductCreatePage.tsx | 209 +- .../ProductDetailsForm/ProductDetailsForm.tsx | 19 +- .../ProductImageNavigation.tsx | 2 +- .../ProductImages/ProductImages.tsx | 28 +- .../ProductOrganization.tsx | 356 +- .../ProductUpdatePage/ProductUpdatePage.tsx | 436 +- .../ProductVariantAttributes.tsx | 212 +- .../ProductVariantCreatePage.tsx | 187 +- .../ProductVariantImageSelectDialog.tsx | 2 +- .../ProductVariantNavigation.tsx | 52 +- .../ProductVariantPage/ProductVariantPage.tsx | 248 +- .../ProductVariants/ProductVariants.tsx | 6 +- src/products/fixtures.ts | 147 +- src/products/mutations.ts | 6 + src/products/queries.ts | 11 +- src/products/types/Product.ts | 9 +- src/products/types/ProductCreate.ts | 10 +- src/products/types/ProductCreateData.ts | 5 +- src/products/types/ProductDetails.ts | 9 +- src/products/types/ProductImageCreate.ts | 9 +- src/products/types/ProductImageUpdate.ts | 9 +- src/products/types/ProductUpdate.ts | 10 +- src/products/types/ProductVariant.ts | 3 +- .../types/ProductVariantCreateData.ts | 2 +- src/products/types/ProductVariantDetails.ts | 3 +- src/products/types/SimpleProductUpdate.ts | 13 +- src/products/types/VariantCreate.ts | 3 +- src/products/types/VariantImageAssign.ts | 3 +- src/products/types/VariantImageUnassign.ts | 3 +- src/products/types/VariantUpdate.ts | 3 +- src/products/views/ProductCreate.tsx | 32 +- .../views/ProductList/ProductList.tsx | 29 +- src/products/views/ProductUpdate.tsx | 2 +- .../views/ProductUpdate/ProductUpdate.tsx | 2 +- src/products/views/ProductVariant.tsx | 25 +- src/products/views/ProductVariantCreate.tsx | 26 +- .../ShippingZonesList/ShippingZonesList.tsx | 20 +- .../ShippingZonesListPage.tsx | 6 +- src/shipping/views/ShippingZonesList.tsx | 16 +- .../SiteSettingsPage/SiteSettingsPage.tsx | 126 +- src/siteSettings/fixtures.ts | 26 + src/siteSettings/mutations.ts | 14 + src/siteSettings/queries.ts | 9 + src/siteSettings/types/AuthorizationKeyAdd.ts | 30 + .../types/AuthorizationKeyDelete.ts | 30 + src/siteSettings/types/ShopFragment.ts | 30 + src/siteSettings/types/ShopSettingsUpdate.ts | 73 +- src/siteSettings/types/SiteSettings.ts | 30 + src/siteSettings/views/index.tsx | 19 +- .../StaffDetailsPage/StaffDetailsPage.tsx | 6 +- src/staff/components/StaffList/StaffList.tsx | 12 +- .../StaffListPage/StaffListPage.tsx | 4 +- .../StaffPermissions/StaffPermissions.tsx | 2 +- .../StaffProperties/StaffProperties.tsx | 2 +- src/staff/views/StaffList.tsx | 17 +- src/storybook/Decorator.tsx | 33 +- src/storybook/Stories.test.ts | 10 +- .../__snapshots__/Stories.test.ts.snap | 37874 ++++++++++++---- src/storybook/config.js | 18 +- .../stories/categories/CategoryProducts.tsx | 2 +- .../stories/categories/CategoryUpdatePage.tsx | 8 +- .../collections/CollectionDetailsPage.tsx | 4 +- .../stories/components/AddressEdit.tsx | 7 +- .../components/AssignProductDialog.tsx | 2 +- .../MultiAutocompleteSelectField.tsx | 117 +- .../SingleAutocompleteSelectField.tsx | 108 +- .../configuration/ConfigurationPage.tsx | 4 + src/storybook/stories/home/HomePage.tsx | 2 +- .../stories/navigation/MenuItemDialog.tsx | 22 +- .../orders/OrderCustomerEditDialog.tsx | 6 +- .../stories/orders/OrderDetailsPage.tsx | 2 +- .../stories/orders/OrderDraftPage.tsx | 2 +- .../stories/orders/OrderFulfillmentDialog.tsx | 2 +- .../stories/orders/OrderProductAddDialog.tsx | 2 +- .../productTypes/ProductTypeCreatePage.tsx | 13 +- .../productTypes/ProductTypeDetailsPage.tsx | 30 +- .../stories/products/ProductCreatePage.tsx | 6 +- .../stories/products/ProductImagePage.tsx | 2 +- .../stories/products/ProductListCard.tsx | 2 +- .../stories/products/ProductListPage.tsx | 2 +- .../stories/products/ProductUpdatePage.tsx | 139 +- .../products/ProductVariantCreatePage.tsx | 39 +- .../ProductVariantImageSelectDialog.tsx | 2 +- .../stories/products/ProductVariantPage.tsx | 17 +- src/storybook/webpack.config.js | 2 +- .../components/CountryList/CountryList.tsx | 2 +- src/theme.ts | 145 +- .../TranslationsEntitiesList.tsx | 2 +- src/types.ts | 46 +- src/types/globalTypes.ts | 72 +- 262 files changed, 35477 insertions(+), 13429 deletions(-) diff --git a/src/auth/components/LoginPage/LoginPage.tsx b/src/auth/components/LoginPage/LoginPage.tsx index 148a02d56..b13d25090 100644 --- a/src/auth/components/LoginPage/LoginPage.tsx +++ b/src/auth/components/LoginPage/LoginPage.tsx @@ -10,14 +10,14 @@ import Typography from "@material-ui/core/Typography"; import React from "react"; import SVG from "react-inlinesvg"; -import * as backgroundArt from "@assets/images/login-background.svg"; -import * as saleorDarkLogo from "@assets/images/logo-dark.svg"; -import * as saleorLightLogo from "@assets/images/logo-light.svg"; +import backgroundArt from "@assets/images/login-background.svg"; +import saleorDarkLogo from "@assets/images/logo-dark.svg"; +import saleorLightLogo from "@assets/images/logo-light.svg"; import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox"; import Form from "@saleor/components/Form"; import { FormSpacer } from "@saleor/components/FormSpacer"; import useTheme from "@saleor/hooks/useTheme"; -import i18n from "../../../i18n"; +import i18n from "@saleor/i18n"; export interface FormData { email: string; diff --git a/src/auth/mutations.ts b/src/auth/mutations.ts index 8d4007ba7..6566fee3f 100644 --- a/src/auth/mutations.ts +++ b/src/auth/mutations.ts @@ -16,6 +16,9 @@ export const fragmentUser = gql` code name } + avatar { + url + } } `; diff --git a/src/auth/types/TokenAuth.ts b/src/auth/types/TokenAuth.ts index a282edc47..3cae7fada 100644 --- a/src/auth/types/TokenAuth.ts +++ b/src/auth/types/TokenAuth.ts @@ -20,6 +20,11 @@ export interface TokenAuth_tokenCreate_user_permissions { name: string; } +export interface TokenAuth_tokenCreate_user_avatar { + __typename: "Image"; + url: string; +} + export interface TokenAuth_tokenCreate_user { __typename: "User"; id: string; @@ -29,6 +34,7 @@ export interface TokenAuth_tokenCreate_user { isStaff: boolean; note: string | null; permissions: (TokenAuth_tokenCreate_user_permissions | null)[] | null; + avatar: TokenAuth_tokenCreate_user_avatar | null; } export interface TokenAuth_tokenCreate { diff --git a/src/auth/types/User.ts b/src/auth/types/User.ts index 6027f7f82..6609b9897 100644 --- a/src/auth/types/User.ts +++ b/src/auth/types/User.ts @@ -14,6 +14,11 @@ export interface User_permissions { name: string; } +export interface User_avatar { + __typename: "Image"; + url: string; +} + export interface User { __typename: "User"; id: string; @@ -23,4 +28,5 @@ export interface User { isStaff: boolean; note: string | null; permissions: (User_permissions | null)[] | null; + avatar: User_avatar | null; } diff --git a/src/auth/types/VerifyToken.ts b/src/auth/types/VerifyToken.ts index 52dc496a4..10e83496e 100644 --- a/src/auth/types/VerifyToken.ts +++ b/src/auth/types/VerifyToken.ts @@ -14,6 +14,11 @@ export interface VerifyToken_tokenVerify_user_permissions { name: string; } +export interface VerifyToken_tokenVerify_user_avatar { + __typename: "Image"; + url: string; +} + export interface VerifyToken_tokenVerify_user { __typename: "User"; id: string; @@ -23,6 +28,7 @@ export interface VerifyToken_tokenVerify_user { isStaff: boolean; note: string | null; permissions: (VerifyToken_tokenVerify_user_permissions | null)[] | null; + avatar: VerifyToken_tokenVerify_user_avatar | null; } export interface VerifyToken_tokenVerify { diff --git a/src/categories/components/CategoryList/CategoryList.tsx b/src/categories/components/CategoryList/CategoryList.tsx index 6b8776b8f..293e04830 100644 --- a/src/categories/components/CategoryList/CategoryList.tsx +++ b/src/categories/components/CategoryList/CategoryList.tsx @@ -18,9 +18,9 @@ import Checkbox from "@saleor/components/Checkbox"; import Skeleton from "@saleor/components/Skeleton"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; -import { renderCollection } from "../../../misc"; -import { ListActions, ListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { renderCollection } from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; const styles = (theme: Theme) => createStyles({ @@ -67,11 +67,14 @@ interface CategoryListProps onAdd?(); } +const numberOfColumns = 4; + const CategoryList = withStyles(styles, { name: "CategoryList" })( ({ categories, classes, disabled, + settings, isRoot, pageInfo, isChecked, @@ -82,6 +85,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })( onAdd, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick }: CategoryListProps) => ( @@ -97,6 +101,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })( )} toggle(category.id)} /> @@ -173,7 +181,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })( }, () => ( - + {isRoot ? i18n.t("No categories found") : i18n.t("No subcategories found")} diff --git a/src/categories/components/CategoryListPage/CategoryListPage.tsx b/src/categories/components/CategoryListPage/CategoryListPage.tsx index e465c255b..c3829c4bd 100644 --- a/src/categories/components/CategoryListPage/CategoryListPage.tsx +++ b/src/categories/components/CategoryListPage/CategoryListPage.tsx @@ -4,8 +4,8 @@ import React from "react"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import i18n from "../../../i18n"; -import { ListActions, PageListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { ListActions, PageListProps } from "@saleor/types"; import CategoryList from "../CategoryList"; export interface CategoryTableProps extends PageListProps, ListActions { @@ -24,9 +24,11 @@ export interface CategoryTableProps extends PageListProps, ListActions { export const CategoryListPage: React.StatelessComponent = ({ categories, disabled, + settings, onAdd, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick, pageInfo, isChecked, @@ -46,9 +48,11 @@ export const CategoryListPage: React.StatelessComponent = ({ onAdd={onAdd} onRowClick={onRowClick} disabled={disabled} + settings={settings} isRoot={true} onNextPage={onNextPage} onPreviousPage={onPreviousPage} + onUpdateListSettings={onUpdateListSettings} pageInfo={pageInfo} isChecked={isChecked} selected={selected} diff --git a/src/categories/components/CategoryProductsCard/CategoryProductsCard.tsx b/src/categories/components/CategoryProductsCard/CategoryProductsCard.tsx index 78f2a2263..e862eb68b 100644 --- a/src/categories/components/CategoryProductsCard/CategoryProductsCard.tsx +++ b/src/categories/components/CategoryProductsCard/CategoryProductsCard.tsx @@ -40,6 +40,10 @@ export const CategoryProductsCard: React.StatelessComponent< } /> = ({ const { isSelected, listElements, toggle, toggleAll, reset } = useBulkActions( params.ids ); - - const paginationState = createPaginationState(PAGINATE_BY, params); + const { updateListSettings, settings } = useListSettings( + ListViews.CATEGORY_LIST + ); + const paginationState = createPaginationState(settings.rowNumber, params); return ( {({ data, loading, refetch }) => { @@ -73,11 +76,13 @@ export const CategoryList: React.StatelessComponent = ({ () => data.categories.edges.map(edge => edge.node), [] )} + settings={settings} onAdd={() => navigate(categoryAddUrl())} onRowClick={id => () => navigate(categoryUrl(id))} disabled={loading} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} pageInfo={pageInfo} isChecked={isSelected} selected={listElements.length} diff --git a/src/collections/components/CollectionList/CollectionList.tsx b/src/collections/components/CollectionList/CollectionList.tsx index 6a4b24d9c..fd63f69a3 100644 --- a/src/collections/components/CollectionList/CollectionList.tsx +++ b/src/collections/components/CollectionList/CollectionList.tsx @@ -17,9 +17,9 @@ import Skeleton from "@saleor/components/Skeleton"; import StatusLabel from "@saleor/components/StatusLabel"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; -import { maybe, renderCollection } from "../../../misc"; -import { ListActions, ListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { maybe, renderCollection } from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; import { CollectionList_collections_edges_node } from "../../types/CollectionList"; const styles = (theme: Theme) => @@ -50,13 +50,17 @@ interface CollectionListProps collections: CollectionList_collections_edges_node[]; } +const numberOfColumns = 5; + const CollectionList = withStyles(styles, { name: "CollectionList" })( ({ classes, collections, disabled, + settings, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick, pageInfo, isChecked, @@ -68,6 +72,7 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
toggle(collection.id)} /> @@ -151,7 +159,7 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })( }, () => ( - + {i18n.t("No collections found")} diff --git a/src/collections/components/CollectionListPage/CollectionListPage.tsx b/src/collections/components/CollectionListPage/CollectionListPage.tsx index d5580c66e..b115569fb 100644 --- a/src/collections/components/CollectionListPage/CollectionListPage.tsx +++ b/src/collections/components/CollectionListPage/CollectionListPage.tsx @@ -4,8 +4,8 @@ import React from "react"; import { Container } from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import i18n from "../../../i18n"; -import { ListActions, PageListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { ListActions, PageListProps } from "@saleor/types"; import { CollectionList_collections_edges_node } from "../../types/CollectionList"; import CollectionList from "../CollectionList/CollectionList"; diff --git a/src/collections/components/CollectionProducts/CollectionProducts.tsx b/src/collections/components/CollectionProducts/CollectionProducts.tsx index 977c1ed45..b957a489a 100644 --- a/src/collections/components/CollectionProducts/CollectionProducts.tsx +++ b/src/collections/components/CollectionProducts/CollectionProducts.tsx @@ -19,7 +19,9 @@ import CardTitle from "@saleor/components/CardTitle"; import Checkbox from "@saleor/components/Checkbox"; import Skeleton from "@saleor/components/Skeleton"; import StatusLabel from "@saleor/components/StatusLabel"; -import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import TableCellAvatar, { + AVATAR_MARGIN +} from "@saleor/components/TableCellAvatar"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; import i18n from "../../../i18n"; @@ -29,12 +31,27 @@ import { CollectionDetails_collection } from "../../types/CollectionDetails"; const styles = (theme: Theme) => createStyles({ - iconCell: { + colActions: { "&:last-child": { paddingRight: 0 }, width: 48 + theme.spacing.unit / 2 }, + colName: { + width: "auto" + }, + colNameLabel: { + marginLeft: AVATAR_MARGIN + }, + colPublished: { + width: 200 + }, + colType: { + width: 200 + }, + table: { + tableLayout: "fixed" + }, tableRow: { cursor: "pointer" } @@ -48,6 +65,8 @@ export interface CollectionProductsProps onProductUnassign: (id: string, event: React.MouseEvent) => void; } +const numberOfColumns = 5; + const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })( ({ classes, @@ -89,24 +108,32 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })( } /> -
+
collection.products.edges.map(edge => edge.node))} toggleAll={toggleAll} toolbar={toolbar} > - {i18n.t("Name", { context: "table header" })} - {i18n.t("Type", { context: "table header" })} - + + + {i18n.t("Name", { context: "table header" })} + + + + {i18n.t("Type", { context: "table header" })} + + {i18n.t("Published", { context: "table header" })} + pageInfo.hasNextPage)} onNextPage={onNextPage} hasPreviousPage={maybe(() => pageInfo.hasPreviousPage)} @@ -132,22 +159,23 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })( toggle(product.id)} /> product.thumbnail.url)} - /> - + > {maybe(() => product.name, )} - - + + {maybe( () => product.productType.name, )} - + {maybe( () => ( )} - + onProductUnassign(product.id, event)} @@ -176,7 +204,9 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })( () => ( - {i18n.t("No products found")} + + {i18n.t("No products found")} + ) )} diff --git a/src/collections/fixtures.ts b/src/collections/fixtures.ts index 423cf82fc..8fc75b09b 100644 --- a/src/collections/fixtures.ts +++ b/src/collections/fixtures.ts @@ -22,6 +22,46 @@ export const collections: CollectionList_collections_edges_node[] = [ __typename: "ProductCountableConnection", totalCount: 4 } + }, + { + __typename: "Collection", + id: "Q29sbGVjdGlvbjoz", + isPublished: true, + name: "Vintage vibes", + products: { + __typename: "ProductCountableConnection", + totalCount: 4 + } + }, + { + __typename: "Collection", + id: "Q29sbGVjdGlvbjoa", + isPublished: true, + name: "Merry Christmas", + products: { + __typename: "ProductCountableConnection", + totalCount: 4 + } + }, + { + __typename: "Collection", + id: "Q29sbGVjdGlvbjob", + isPublished: true, + name: "80s Miami", + products: { + __typename: "ProductCountableConnection", + totalCount: 4 + } + }, + { + __typename: "Collection", + id: "Q29sbGVjdGlvbjoc", + isPublished: true, + name: "Yellow Submarine 2019", + products: { + __typename: "ProductCountableConnection", + totalCount: 4 + } } ]; export const collection: ( diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index 26cb4b48e..bdabd01c1 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -287,14 +287,12 @@ export const CollectionDetails: React.StatelessComponent< open={params.action === "assign"} onFetch={search} loading={result.loading} - onClose={() => navigate(collectionUrl(id), true, true)} - onSubmit={formData => + onClose={closeModal} + onSubmit={products => assignProduct.mutate({ ...paginationState, collectionId: id, - productIds: formData.products.map( - product => product.id - ) + productIds: products.map(product => product.id) }) } products={maybe(() => diff --git a/src/collections/views/CollectionList.tsx b/src/collections/views/CollectionList.tsx index 008fcfb4d..90a3f6686 100644 --- a/src/collections/views/CollectionList.tsx +++ b/src/collections/views/CollectionList.tsx @@ -6,14 +6,15 @@ import React from "react"; import ActionDialog from "@saleor/components/ActionDialog"; import useBulkActions from "@saleor/hooks/useBulkActions"; +import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; -import { PAGINATE_BY } from "../../config"; -import i18n from "../../i18n"; -import { getMutationState, maybe } from "../../misc"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; import CollectionListPage from "../components/CollectionListPage/CollectionListPage"; import { TypedCollectionBulkDelete, @@ -43,6 +44,9 @@ export const CollectionList: React.StatelessComponent = ({ const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); + const { updateListSettings, settings } = useListSettings( + ListViews.COLLECTION_LIST + ); const closeModal = () => navigate( @@ -62,7 +66,7 @@ export const CollectionList: React.StatelessComponent = ({ }) ); - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); return ( {({ data, loading, refetch }) => { @@ -129,8 +133,10 @@ export const CollectionList: React.StatelessComponent = ({ collections={maybe(() => data.collections.edges.map(edge => edge.node) )} + settings={settings} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} pageInfo={pageInfo} onRowClick={id => () => navigate(collectionUrl(id))} toolbar={ diff --git a/src/components/ActionDialog/ActionDialog.tsx b/src/components/ActionDialog/ActionDialog.tsx index 6387d4ec6..3a5df3a46 100644 --- a/src/components/ActionDialog/ActionDialog.tsx +++ b/src/components/ActionDialog/ActionDialog.tsx @@ -9,7 +9,7 @@ import { withStyles, WithStyles } from "@material-ui/core/styles"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import i18n from "../../i18n"; diff --git a/src/components/AddressEdit/AddressEdit.tsx b/src/components/AddressEdit/AddressEdit.tsx index 428340604..7960d3180 100644 --- a/src/components/AddressEdit/AddressEdit.tsx +++ b/src/components/AddressEdit/AddressEdit.tsx @@ -7,12 +7,13 @@ import { import TextField from "@material-ui/core/TextField"; import React from "react"; -import { AddressTypeInput } from "../../customers/types"; -import i18n from "../../i18n"; -import { maybe } from "../../misc"; -import { FormErrors } from "../../types"; +import { AddressTypeInput } from "@saleor/customers/types"; +import i18n from "@saleor/i18n"; +import { FormErrors } from "@saleor/types"; import FormSpacer from "../FormSpacer"; -import SingleAutocompleteSelectField from "../SingleAutocompleteSelectField"; +import SingleAutocompleteSelectField, { + SingleAutocompleteChoiceType +} from "../SingleAutocompleteSelectField"; const styles = (theme: Theme) => createStyles({ @@ -24,24 +25,25 @@ const styles = (theme: Theme) => }); interface AddressEditProps extends WithStyles { - countries?: Array<{ - code: string; - label: string; - }>; + countries: SingleAutocompleteChoiceType[]; + countryDisplayValue: string; data: AddressTypeInput; disabled?: boolean; errors: FormErrors; onChange(event: React.ChangeEvent); + onCountryChange(event: React.ChangeEvent); } const AddressEdit = withStyles(styles, { name: "AddressEdit" })( ({ classes, countries, + countryDisplayValue, data, disabled, errors, - onChange + onChange, + onCountryChange }: AddressEditProps) => ( <>
@@ -152,16 +154,14 @@ const AddressEdit = withStyles(styles, { name: "AddressEdit" })(
countries.map(c => ({ ...c, value: c.code })), - [] - )} + choices={countries} InputProps={{ autoComplete: "off" }} diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index 35aaf7983..350b12752 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -1,6 +1,8 @@ +import Avatar from "@material-ui/core/Avatar"; import Chip from "@material-ui/core/Chip"; import ClickAwayListener from "@material-ui/core/ClickAwayListener"; import Grow from "@material-ui/core/Grow"; +import Hidden from "@material-ui/core/Hidden"; import LinearProgress from "@material-ui/core/LinearProgress"; import MenuItem from "@material-ui/core/MenuItem"; import Menu from "@material-ui/core/MenuList"; @@ -12,23 +14,25 @@ import { withStyles, WithStyles } from "@material-ui/core/styles"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import SVG from "react-inlinesvg"; import { RouteComponentProps, withRouter } from "react-router"; -import * as saleorDarkLogo from "@assets/images/logo-dark.svg"; -import * as saleorLightLogo from "@assets/images/logo-light.svg"; +import saleorDarkLogoSmall from "@assets/logo-dark-small.svg"; +import saleorDarkLogo from "@assets/logo-dark.svg"; +import menuArrowIcon from "@assets/menu-arrow-icon.svg"; import AppProgressProvider from "@saleor/components/AppProgress"; +import useLocalStorage from "@saleor/hooks/useLocalStorage"; import useNavigator from "@saleor/hooks/useNavigator"; import useTheme from "@saleor/hooks/useTheme"; import useUser from "@saleor/hooks/useUser"; -import i18n from "../../i18n"; -import ArrowDropdown from "../../icons/ArrowDropdown"; +import i18n from "@saleor/i18n"; +import ArrowDropdown from "@saleor/icons/ArrowDropdown"; import Container from "../Container"; import AppActionContext from "./AppActionContext"; import AppHeaderContext from "./AppHeaderContext"; -import { appLoaderHeight, drawerWidth } from "./consts"; +import { appLoaderHeight, drawerWidth, drawerWidthExpanded } from "./consts"; import MenuList from "./MenuList"; import menuStructure from "./menuStructure"; import ResponsiveDrawer from "./ResponsiveDrawer"; @@ -51,9 +55,18 @@ const styles = (theme: Theme) => transition: theme.transitions.duration.standard + "ms" }, content: { - display: "flex", - flexDirection: "column", - minHeight: `calc(100vh - ${appLoaderHeight}px)` + [theme.breakpoints.down("sm")]: { + paddingLeft: 0 + }, + paddingLeft: drawerWidthExpanded, + transition: "padding-left 0.5s ease", + width: "100%" + }, + contentToggle: { + [theme.breakpoints.down("sm")]: { + paddingLeft: 0 + }, + paddingLeft: drawerWidth }, darkThemeSwitch: { marginRight: theme.spacing.unit * 2 @@ -67,20 +80,72 @@ const styles = (theme: Theme) => hide: { opacity: 0 }, + isMenuSmall: { + "& path": { + fill: theme.palette.primary.main + }, + "& span": { + margin: "0 8px" + }, + "& svg": { + marginTop: 12, + transform: "rotate(180deg)" + }, + "&:hover": { + background: "#E6F3F3" + }, + background: theme.palette.background.paper, + border: `solid 1px #EAEAEA`, + borderRadius: "50%", + cursor: "pointer", + height: 32, + position: "absolute", + right: -16, + top: 65, + transition: `background ${theme.transitions.duration.shorter}ms`, + width: 32, + zIndex: 99 + }, + isMenuSmallDark: { + "&:hover": { + background: `linear-gradient(0deg, rgba(25, 195, 190, 0.1), rgba(25, 195, 190, 0.1)), ${ + theme.palette.background.paper + }` + }, + border: `solid 1px #252728`, + transition: `background ${theme.transitions.duration.shorter}ms` + }, + isMenuSmallHide: { + "& svg": { + transform: "rotate(0deg)" + } + }, logo: { "& svg": { - height: "100%" + height: "100%", + margin: "20px 50px" }, + background: theme.palette.secondary.main, display: "block", - height: 28 + height: 80 + }, + logoDark: { + "& path": { + fill: theme.palette.common.white + }, + background: theme.palette.primary.main + }, + logoSmall: { + "& svg": { + margin: "0px 25px" + } }, menu: { - marginTop: theme.spacing.unit * 4 + background: theme.palette.background.paper, + height: "100vh", + padding: 25 }, menuIcon: { - [theme.breakpoints.up("md")]: { - display: "none" - }, "& span": { "&:nth-child(1)": { top: 15 @@ -101,6 +166,9 @@ const styles = (theme: Theme) => transition: ".25s ease-in-out", width: "60%" }, + [theme.breakpoints.up("md")]: { + display: "none" + }, background: theme.palette.background.paper, borderRadius: "50%", cursor: "pointer", @@ -109,7 +177,7 @@ const styles = (theme: Theme) => marginRight: theme.spacing.unit * 2, position: "relative", transform: "rotate(0deg)", - transition: ".2s ease-in-out", + transition: `${theme.transitions.duration.shorter}ms ease-in-out`, width: 42 }, menuIconDark: { @@ -135,15 +203,16 @@ const styles = (theme: Theme) => position: "absolute", zIndex: 1999 }, + menuSmall: { + background: theme.palette.background.paper, + height: "100vh", + padding: 25 + }, popover: { zIndex: 1 }, root: { - [theme.breakpoints.down("sm")]: { - gridTemplateColumns: "1fr" - }, - display: "grid", - gridTemplateColumns: `${drawerWidth}px 1fr` + width: `100%` }, rotate: { transform: "rotate(180deg)" @@ -153,7 +222,7 @@ const styles = (theme: Theme) => padding: 0 }, background: theme.palette.background.paper, - padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 4}px` + padding: `0 ${theme.spacing.unit * 4}px` }, spacer: { flex: 1 @@ -163,8 +232,8 @@ const styles = (theme: Theme) => display: "flex" }, userChip: { - backgroundColor: theme.palette.common.white, - border: `1px solid ${theme.palette.grey[200]}` + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary }, userMenuContainer: { position: "relative" @@ -191,7 +260,7 @@ interface AppLayoutProps { const AppLayout = withStyles(styles, { name: "AppLayout" })( - withRouter>( + withRouter, any>( ({ classes, children, @@ -200,6 +269,7 @@ const AppLayout = withStyles(styles, { WithStyles & RouteComponentProps) => { const { isDark, toggleTheme } = useTheme(); + const [isMenuSmall, setMenuSmall] = useLocalStorage("isMenuSmall", false); const [isDrawerOpened, setDrawerState] = React.useState(false); const [isMenuOpened, setMenuState] = React.useState(false); const appActionAnchor = React.useRef(); @@ -223,6 +293,10 @@ const AppLayout = withStyles(styles, { navigate(url); }; + const handleIsMenuSmall = () => { + setMenuSmall(!isMenuSmall); + }; + return ( {({ isProgress }) => ( @@ -239,14 +313,37 @@ const AppLayout = withStyles(styles, { setDrawerState(false)} open={isDrawerOpened} + small={!isMenuSmall} > - +
+ +
+ +
+ +
+
-
+
@@ -283,6 +384,11 @@ const AppLayout = withStyles(styles, { ref={anchor} > + ) + } className={classes.userChip} label={ <> diff --git a/src/components/AppLayout/MenuList.tsx b/src/components/AppLayout/MenuList.tsx index 994e0225d..96eb2999c 100644 --- a/src/components/AppLayout/MenuList.tsx +++ b/src/components/AppLayout/MenuList.tsx @@ -7,8 +7,11 @@ import { import Typography from "@material-ui/core/Typography"; import classNames from "classnames"; import React from "react"; +import SVG from "react-inlinesvg"; import { matchPath } from "react-router"; +import useTheme from "@saleor/hooks/useTheme"; +import configureIcon from "@assets/images/menu-configure-icon.svg"; import { User } from "../../auth/types/User"; import { configurationMenu, configurationMenuUrl } from "../../configuration"; import i18n from "../../i18n"; @@ -19,6 +22,44 @@ import { IMenuItem } from "./menuStructure"; const styles = (theme: Theme) => createStyles({ + menuIcon: { + "& svg": { + height: 32, + width: 32 + }, + display: "inline-block", + position: "relative", + top: 8 + }, + menuIconDark: { + "& path": { + fill: theme.palette.common.white + } + }, + menuIsActive: { + boxShadow: "0px 0px 12px 1px rgba(0,0,0,0.2)" + }, + menuItemHover: { + "& path": { + transition: "fill 0.5s ease" + }, + "&:hover": { + "& path": { + fill: theme.palette.primary.main + }, + "&:before": { + borderLeft: `solid 2px ${theme.palette.primary.main}`, + content: "''", + height: 33, + left: -25, + position: "absolute", + top: 8 + }, + color: theme.palette.primary.main + }, + cursor: "pointer", + position: "relative" + }, menuList: { display: "flex", flexDirection: "column", @@ -28,24 +69,33 @@ const styles = (theme: Theme) => paddingBottom: theme.spacing.unit * 3 }, menuListItem: { - "&:hover": { - color: theme.palette.primary.main - }, alignItems: "center", display: "block", - marginTop: theme.spacing.unit * 2, + marginBottom: theme.spacing.unit * 5, paddingLeft: 0, textDecoration: "none", transition: theme.transitions.duration.standard + "ms" }, menuListItemActive: { - "&:before": { - background: theme.palette.primary.main, + "& $menuListItemText": { + color: theme.palette.primary.main + }, + "& path": { + color: theme.palette.primary.main, + fill: theme.palette.primary.main + } + }, + menuListItemOpen: { + "&:after": { + borderBottom: `10px solid transparent`, + borderLeft: `10px solid ${theme.palette.background.paper}`, + borderTop: `10px solid transparent`, content: "''", - height: "100%", - left: -32, + height: 0, position: "absolute", - width: 5 + right: -35, + top: 15, + width: 0 }, position: "relative" }, @@ -54,123 +104,231 @@ const styles = (theme: Theme) => color: theme.palette.primary.main }, cursor: "pointer", + display: "inline-block", fontSize: "1rem", fontWeight: 500, + opacity: 1, + paddingLeft: 16, textTransform: "uppercase", - transition: theme.transitions.duration.standard + "ms" + transition: `opacity ${theme.transitions.duration.shorter}ms ease 0.1s` }, - menuListNested: { - "& $menuListItemActive": { - "& $menuListItemText": { - color: theme.palette.primary.main - }, - "&:before": { - borderRadius: "100%", - height: 8, - marginLeft: 9, - marginTop: 7, - width: 8 - } - }, - "& $menuListItemText": { - textTransform: "none" - }, - marginLeft: theme.spacing.unit * 3 + menuListItemTextHide: { + opacity: 0, + position: "absolute", + transition: `opacity ${theme.transitions.duration.shorter}ms ease` + }, + subMenu: { + padding: "0 15px" + }, + subMenuDrawer: { + background: "#000", + cursor: "pointer", + height: "100vh", + left: 0, + opacity: 0.2, + position: "absolute", + top: 0, + width: 0, + zIndex: -2 + }, + subMenuDrawerOpen: { + width: `100vw` } }); interface MenuListProps { className?: string; menuItems: IMenuItem[]; + isMenuSmall: boolean; location: string; user: User; renderConfigure: boolean; onMenuItemClick: (url: string, event: React.MouseEvent) => void; } + +export interface IActiveSubMenu { + isActive: boolean; + label: string | null; +} + const MenuList = withStyles(styles, { name: "MenuList" })( ({ classes, className, menuItems, + isMenuSmall, location, user, renderConfigure, onMenuItemClick - }: MenuListProps & WithStyles) => ( -
- {/* FIXME: this .split("?")[0] looks gross */} - {menuItems.map(menuItem => { - const isActive = (menuItem: IMenuItem) => - location.split("?")[0] === orderDraftListUrl().split("?")[0] && - menuItem.url.split("?")[0] === orderListUrl().split("?")[0] - ? false - : !!matchPath(location.split("?")[0], { - exact: menuItem.url.split("?")[0] === "/", - path: menuItem.url.split("?")[0] - }); + }: MenuListProps & WithStyles) => { + const { isDark } = useTheme(); + const [activeSubMenu, setActiveSubMenu] = React.useState({ + isActive: false, + label: null + }); - if ( - menuItem.permission && - !user.permissions.map(perm => perm.code).includes(menuItem.permission) - ) { - return null; - } + const handleSubMenu = itemLabel => { + setActiveSubMenu({ + isActive: + itemLabel === activeSubMenu.label ? !activeSubMenu.isActive : true, + label: itemLabel + }); + }; - if (!menuItem.url) { - const isAnyChildActive = menuItem.children.reduce( - (acc, child) => acc || isActive(child), - false - ); + const closeSubMenu = (menuItemUrl, event) => { + setActiveSubMenu({ + isActive: false, + label: null + }); + if (menuItemUrl && event) { + onMenuItemClick(menuItemUrl, event); + event.stopPropagation(); + event.preventDefault(); + } + }; + + return ( +
+ {/* FIXME: this .split("?")[0] looks gross */} + {menuItems.map(menuItem => { + const isActive = (menuItem: IMenuItem) => + location.split("?")[0] === orderDraftListUrl().split("?")[0] && + menuItem.url.split("?")[0] === orderListUrl().split("?")[0] + ? false + : !!matchPath(location.split("?")[0], { + exact: menuItem.url.split("?")[0] === "/", + path: menuItem.url.split("?")[0] + }); + + if ( + menuItem.permission && + !user.permissions + .map(perm => perm.code) + .includes(menuItem.permission) + ) { + return null; + } + + if (!menuItem.url) { + const isAnyChildActive = menuItem.children.reduce( + (acc, child) => acc || isActive(child), + false + ); + + return ( +
+
handleSubMenu(menuItem.ariaLabel)} + > + + + {menuItem.label} + +
+ +
closeSubMenu(null, event)} + className={classNames(classes.subMenuDrawer, { + [classes.subMenuDrawerOpen]: activeSubMenu.isActive + })} + /> +
+ ); + } return ( - closeSubMenu(menuItem.url, event)} key={menuItem.label} - /> + > +
+ + + {menuItem.label} + +
+ ); - } - - return ( - onMenuItemClick(menuItem.url, event)} - key={menuItem.label} - > - + user.permissions + .map(perm => perm.code) + .includes(menuItem.permission) + ).length > 0 && ( + onMenuItemClick(configurationMenuUrl, event)} > - {menuItem.label} - - - ); - })} - {renderConfigure && - configurationMenu.filter(menuItem => - user.permissions.map(perm => perm.code).includes(menuItem.permission) - ).length > 0 && ( - onMenuItemClick(configurationMenuUrl, event)} - > - - {i18n.t("Configure")} - - - )} -
- ) +
+ + + {i18n.t("Configure")} + +
+ + )} +
+ ); + } ); export default MenuList; diff --git a/src/components/AppLayout/MenuNested.tsx b/src/components/AppLayout/MenuNested.tsx index afa63fd12..795a759d1 100644 --- a/src/components/AppLayout/MenuNested.tsx +++ b/src/components/AppLayout/MenuNested.tsx @@ -1,61 +1,195 @@ +import Hidden from "@material-ui/core/Hidden"; +import { + createStyles, + Theme, + withStyles, + WithStyles +} from "@material-ui/core/styles"; import Typography from "@material-ui/core/Typography"; import classNames from "classnames"; import React from "react"; +import SVG from "react-inlinesvg"; -import { User } from "../../auth/types/User"; -import MenuList from "./MenuList"; +import menuArrowIcon from "@assets/images/menu-arrow-icon.svg"; +import useTheme from "@saleor/hooks/useTheme"; +import { createHref } from "@saleor/misc"; +import { drawerWidthExpanded } from "./consts"; +import { IActiveSubMenu } from "./MenuList"; import { IMenuItem } from "./menuStructure"; +const styles = (theme: Theme) => + createStyles({ + menuListNested: { + background: theme.palette.background.paper, + height: "100vh", + position: "absolute", + right: 0, + top: 0, + transition: `right ${theme.transitions.duration.shorter}ms ease`, + width: 300, + zIndex: -1 + }, + menuListNestedClose: { + "& svg": { + fill: theme.palette.primary.main, + left: 7, + position: "relative", + top: -2 + }, + border: `solid 1px #EAEAEA`, + borderRadius: "100%", + cursor: "pointer", + height: 32, + position: "absolute", + right: 32, + top: 35, + transform: "rotate(180deg)", + width: 32 + }, + menuListNestedCloseDark: { + border: `solid 1px #252728` + }, + menuListNestedHide: { + opacity: 0 + }, + menuListNestedIcon: { + "& path": { + fill: "initial" + }, + "& svg": { height: 32, position: "relative", top: 7, width: 32 } + }, + menuListNestedIconDark: { + "& path": { + fill: theme.palette.common.white + } + }, + menuListNestedItem: { + "&:hover": { + "& p": { + color: theme.palette.primary.main + } + }, + display: "block", + marginBottom: theme.spacing.unit * 2, + padding: "0px 30px", + textDecoration: "none" + }, + menuListNestedOpen: { + [theme.breakpoints.down("sm")]: { + right: 0, + width: drawerWidthExpanded, + zIndex: 2 + }, + right: -300, + zIndex: -1 + }, + subHeader: { + borderBottom: "solid 1px #EAEAEA", + margin: "30px", + marginBottom: 39, + paddingBottom: 22 + }, + subHeaderDark: { + borderBottom: "solid 1px #252728" + }, + subHeaderTitle: { + [theme.breakpoints.up("md")]: { + paddingLeft: 0 + }, + display: "inline", + paddingLeft: 10 + } + }); + export interface MenuNestedProps { - classes: Record< - | "menuListItem" - | "menuListItemActive" - | "menuListItemText" - | "menuListNested", - string - >; - isAnyChildActive: boolean; + activeItem: IActiveSubMenu; + ariaLabel: string; + closeSubMenu: ({ isActive, label }: IActiveSubMenu) => void; + icon: string; menuItem: IMenuItem; - location: string; - user: User; + title: string; + handleSubMenu: (itemLabel: string) => void; onMenuItemClick: (url: string, event: React.MouseEvent) => void; } -const MenuNested: React.FC = ({ - classes, - isAnyChildActive, - location, - menuItem, - onMenuItemClick, - user -}) => { - const [isOpened, setOpenStatus] = React.useState(false); - - return ( -
setOpenStatus(!isOpened)} - className={classNames(classes.menuListItem, { - [classes.menuListItemActive]: isAnyChildActive - })} - > - - {menuItem.label} - - {isOpened && ( -
- +const MenuNested = withStyles(styles, { name: "MenuNested" })( + ({ + activeItem, + ariaLabel, + classes, + closeSubMenu, + icon, + menuItem, + onMenuItemClick, + title + }: MenuNestedProps & WithStyles) => { + const menuItems = menuItem.children; + const { isDark } = useTheme(); + const closeMenu = (menuItemUrl, event) => { + onMenuItemClick(menuItemUrl, event); + closeSubMenu({ + isActive: false, + label: null + }); + event.stopPropagation(); + event.preventDefault(); + }; + return ( + <> +
+ + + + +
{title}
+ +
+ closeSubMenu({ + isActive: false, + label: null + }) + } + > + +
+
+
+ {menuItems.map(item => { + return ( + closeMenu(item.url, event)} + key={item.label} + > + + {item.label} + + + ); + })}
- )} -
- ); -}; + + ); + } +); export default MenuNested; diff --git a/src/components/AppLayout/ResponsiveDrawer.tsx b/src/components/AppLayout/ResponsiveDrawer.tsx index 49fac57c1..cc8e026c0 100644 --- a/src/components/AppLayout/ResponsiveDrawer.tsx +++ b/src/components/AppLayout/ResponsiveDrawer.tsx @@ -7,7 +7,7 @@ import { WithStyles } from "@material-ui/core/styles"; import React from "react"; -import { drawerWidth } from "./consts"; +import { drawerWidth, drawerWidthExpanded } from "./consts"; const styles = (theme: Theme) => createStyles({ @@ -15,31 +15,38 @@ const styles = (theme: Theme) => backgroundColor: theme.palette.background.paper, border: "none", height: "100vh", - padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 4}px`, + overflow: "visible", + padding: 0, position: "fixed" as "fixed", + transition: "width 0.2s ease", + width: drawerWidthExpanded + }, + drawerDesktopSmall: { + overflow: "visible", + transition: "width 0.2s ease", width: drawerWidth }, drawerMobile: { - padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 4}px`, - width: drawerWidth, + width: drawerWidthExpanded } }); interface ResponsiveDrawerProps extends WithStyles { children?: React.ReactNode; open: boolean; + small: boolean; onClose?(); } const ResponsiveDrawer = withStyles(styles, { name: "ResponsiveDrawer" })( - ({ children, classes, onClose, open }: ResponsiveDrawerProps) => ( + ({ children, classes, onClose, open, small }: ResponsiveDrawerProps) => ( <> {children} diff --git a/src/components/AppLayout/consts.ts b/src/components/AppLayout/consts.ts index eb1ecfeef..a6d98fbba 100644 --- a/src/components/AppLayout/consts.ts +++ b/src/components/AppLayout/consts.ts @@ -1,3 +1,4 @@ -export const drawerWidth = 256; +export const drawerWidthExpanded = 256; +export const drawerWidth = 80; export const navigationBarHeight = 64; export const appLoaderHeight = 4; diff --git a/src/components/AppLayout/menuStructure.ts b/src/components/AppLayout/menuStructure.ts index 1ff572241..1c13c5dfa 100644 --- a/src/components/AppLayout/menuStructure.ts +++ b/src/components/AppLayout/menuStructure.ts @@ -8,9 +8,17 @@ import { productListUrl } from "../../products/urls"; import { languageListUrl } from "../../translations/urls"; import { PermissionEnum } from "../../types/globalTypes"; +import catalogIcon from "@assets/images/menu-catalog-icon.svg"; +import customerIcon from "@assets/images/menu-customers-icon.svg"; +import discountsIcon from "@assets/images/menu-discounts-icon.svg"; +import homeIcon from "@assets/images/menu-home-icon.svg"; +import ordersIcon from "@assets/images/menu-orders-icon.svg"; +import translationIcon from "@assets/images/menu-translation-icon.svg"; + export interface IMenuItem { ariaLabel: string; children?: IMenuItem[]; + icon?: any; label: string; permission?: PermissionEnum; url?: string; @@ -19,6 +27,7 @@ export interface IMenuItem { const menuStructure: IMenuItem[] = [ { ariaLabel: "home", + icon: homeIcon, label: i18n.t("Home", { context: "Menu label" }), url: "/" }, @@ -41,6 +50,7 @@ const menuStructure: IMenuItem[] = [ url: collectionListUrl() } ], + icon: catalogIcon, label: i18n.t("Catalog", { context: "Menu label" }), permission: PermissionEnum.MANAGE_PRODUCTS }, @@ -60,11 +70,13 @@ const menuStructure: IMenuItem[] = [ url: orderDraftListUrl() } ], + icon: ordersIcon, label: i18n.t("Orders", { context: "Menu label" }), permission: PermissionEnum.MANAGE_ORDERS }, { ariaLabel: "customers", + icon: customerIcon, label: i18n.t("Customers", { context: "Menu label" }), permission: PermissionEnum.MANAGE_USERS, url: customerListUrl() @@ -84,11 +96,13 @@ const menuStructure: IMenuItem[] = [ url: voucherListUrl() } ], + icon: discountsIcon, label: i18n.t("Discounts", { context: "Menu label" }), permission: PermissionEnum.MANAGE_DISCOUNTS }, { ariaLabel: "translations", + icon: translationIcon, label: i18n.t("Translations", { context: "Menu label" }), permission: PermissionEnum.MANAGE_TRANSLATIONS, url: languageListUrl diff --git a/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx b/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx index a272a7da8..b8504b729 100644 --- a/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx +++ b/src/components/AssignCategoryDialog/AssignCategoryDialog.tsx @@ -15,8 +15,8 @@ import React from "react"; import ConfirmButton, { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import Form from "@saleor/components/Form"; import FormSpacer from "@saleor/components/FormSpacer"; +import useSearchQuery from "@saleor/hooks/useSearchQuery"; import { SearchCategories_categories_edges_node } from "../../containers/SearchCategories/types/SearchCategories"; import i18n from "../../i18n"; import Checkbox from "../Checkbox"; @@ -50,13 +50,28 @@ interface AssignCategoriesDialogProps extends WithStyles { loading: boolean; onClose: () => void; onFetch: (value: string) => void; - onSubmit: (data: FormData) => void; + onSubmit: (data: SearchCategories_categories_edges_node[]) => void; +} + +function handleCategoryAssign( + product: SearchCategories_categories_edges_node, + isSelected: boolean, + selectedCategories: SearchCategories_categories_edges_node[], + setSelectedCategories: ( + data: SearchCategories_categories_edges_node[] + ) => void +) { + if (isSelected) { + setSelectedCategories( + selectedCategories.filter( + selectedProduct => selectedProduct.id !== product.id + ) + ); + } else { + setSelectedCategories([...selectedCategories, product]); + } } -const initialForm: FormData = { - categories: [], - query: "" -}; const AssignCategoriesDialog = withStyles(styles, { name: "AssignCategoriesDialog" })( @@ -69,104 +84,93 @@ const AssignCategoriesDialog = withStyles(styles, { onClose, onFetch, onSubmit - }: AssignCategoriesDialogProps) => ( - -
- {({ data, change }) => ( - <> - {i18n.t("Assign Categories")} - - change(event, () => onFetch(data.query))} - label={i18n.t("Search Categories", { - context: "product search input label" - })} - placeholder={i18n.t( - "Search by product name, attribute, product type etc...", - { - context: "product search input placeholder" - } - )} - fullWidth - InputProps={{ - autoComplete: "off", - endAdornment: loading && - }} - /> - -
- - {categories && - categories.map(category => { - const isChecked = !!data.categories.find( - selectedCategories => - selectedCategories.id === category.id - ); + }: AssignCategoriesDialogProps) => { + const [query, onQueryChange] = useSearchQuery(onFetch); + const [selectedCategories, setSelectedCategories] = React.useState< + SearchCategories_categories_edges_node[] + >([]); - return ( - - - - isChecked - ? change({ - target: { - name: "categories", - value: data.categories.filter( - selectedCategories => - selectedCategories.id !== - category.id - ) - } - } as any) - : change({ - target: { - name: "categories", - value: [...data.categories, category] - } - } as any) - } - /> - - - {category.name} - - - ); - })} - -
- - - - - {i18n.t("Assign categories", { context: "button" })} - - - - )} - - - ) + const handleSubmit = () => onSubmit(selectedCategories); + + return ( + + {i18n.t("Assign Categories")} + + + }} + /> + + + + {categories && + categories.map(category => { + const isSelected = !!selectedCategories.find( + selectedCategories => selectedCategories.id === category.id + ); + + return ( + + + + handleCategoryAssign( + category, + isSelected, + selectedCategories, + setSelectedCategories + ) + } + /> + + + {category.name} + + + ); + })} + +
+
+ + + + {i18n.t("Assign categories", { context: "button" })} + + +
+ ); + } ); AssignCategoriesDialog.displayName = "AssignCategoriesDialog"; export default AssignCategoriesDialog; diff --git a/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx b/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx index 6d0780427..a8e316a4a 100644 --- a/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx +++ b/src/components/AssignCollectionDialog/AssignCollectionDialog.tsx @@ -12,13 +12,13 @@ import TableRow from "@material-ui/core/TableRow"; import TextField from "@material-ui/core/TextField"; import React from "react"; +import useSearchQuery from "@saleor/hooks/useSearchQuery"; +import i18n from "@saleor/i18n"; import { SearchCollections_collections_edges_node } from "../../containers/SearchCollections/types/SearchCollections"; -import i18n from "../../i18n"; import Checkbox from "../Checkbox"; import ConfirmButton, { ConfirmButtonTransitionState } from "../ConfirmButton/ConfirmButton"; -import Form from "../Form"; import FormSpacer from "../FormSpacer"; export interface FormData { @@ -50,13 +50,28 @@ interface AssignCollectionDialogProps extends WithStyles { loading: boolean; onClose: () => void; onFetch: (value: string) => void; - onSubmit: (data: FormData) => void; + onSubmit: (data: SearchCollections_collections_edges_node[]) => void; +} + +function handleCollectionAssign( + product: SearchCollections_collections_edges_node, + isSelected: boolean, + selectedCollections: SearchCollections_collections_edges_node[], + setSelectedCollections: ( + data: SearchCollections_collections_edges_node[] + ) => void +) { + if (isSelected) { + setSelectedCollections( + selectedCollections.filter( + selectedProduct => selectedProduct.id !== product.id + ) + ); + } else { + setSelectedCollections([...selectedCollections, product]); + } } -const initialForm: FormData = { - collections: [], - query: "" -}; const AssignCollectionDialog = withStyles(styles, { name: "AssignCollectionDialog" })( @@ -69,104 +84,94 @@ const AssignCollectionDialog = withStyles(styles, { onClose, onFetch, onSubmit - }: AssignCollectionDialogProps) => ( - -
- {({ data, change }) => ( - <> - {i18n.t("Assign Collection")} - - change(event, () => onFetch(data.query))} - label={i18n.t("Search Collection", { - context: "product search input label" - })} - placeholder={i18n.t( - "Search by product name, attribute, product type etc...", - { - context: "product search input placeholder" - } - )} - fullWidth - InputProps={{ - autoComplete: "off", - endAdornment: loading && - }} - /> - - - - {collections && - collections.map(category => { - const isChecked = !!data.collections.find( - selectedCollection => - selectedCollection.id === category.id - ); + }: AssignCollectionDialogProps) => { + const [query, onQueryChange] = useSearchQuery(onFetch); + const [selectedCollections, setSelectedCollections] = React.useState< + SearchCollections_collections_edges_node[] + >([]); - return ( - - - - isChecked - ? change({ - target: { - name: "collections", - value: data.collections.filter( - selectedCollection => - selectedCollection.id !== - category.id - ) - } - } as any) - : change({ - target: { - name: "collections", - value: [...data.collections, category] - } - } as any) - } - /> - - - {category.name} - - - ); - })} - -
-
- - - - {i18n.t("Assign collections", { context: "button" })} - - - - )} -
-
- ) + const handleSubmit = () => onSubmit(selectedCollections); + + return ( + + {i18n.t("Assign Collection")} + + + }} + /> + + + + {collections && + collections.map(collection => { + const isSelected = !!selectedCollections.find( + selectedCollection => + selectedCollection.id === collection.id + ); + + return ( + + + + handleCollectionAssign( + collection, + isSelected, + selectedCollections, + setSelectedCollections + ) + } + /> + + + {collection.name} + + + ); + })} + +
+
+ + + + {i18n.t("Assign collections", { context: "button" })} + + +
+ ); + } ); AssignCollectionDialog.displayName = "AssignCollectionDialog"; export default AssignCollectionDialog; diff --git a/src/components/AssignProductDialog/AssignProductDialog.tsx b/src/components/AssignProductDialog/AssignProductDialog.tsx index 9b975d681..8e3bdfddf 100644 --- a/src/components/AssignProductDialog/AssignProductDialog.tsx +++ b/src/components/AssignProductDialog/AssignProductDialog.tsx @@ -15,12 +15,12 @@ import React from "react"; import ConfirmButton, { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import Form from "@saleor/components/Form"; import FormSpacer from "@saleor/components/FormSpacer"; import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import useSearchQuery from "@saleor/hooks/useSearchQuery"; +import i18n from "@saleor/i18n"; +import { maybe } from "@saleor/misc"; import { SearchProducts_products_edges_node } from "../../containers/SearchProducts/types/SearchProducts"; -import i18n from "../../i18n"; -import { maybe } from "../../misc"; import Checkbox from "../Checkbox"; export interface FormData { @@ -40,25 +40,41 @@ const styles = createStyles({ overflow: { overflowY: "visible" }, + scrollArea: { + overflowY: "scroll" + }, wideCell: { width: "100%" } }); -interface AssignProductDialogProps extends WithStyles { +export interface AssignProductDialogProps { confirmButtonState: ConfirmButtonTransitionState; open: boolean; products: SearchProducts_products_edges_node[]; loading: boolean; onClose: () => void; onFetch: (value: string) => void; - onSubmit: (data: FormData) => void; + onSubmit: (data: SearchProducts_products_edges_node[]) => void; +} + +function handleProductAssign( + product: SearchProducts_products_edges_node, + isSelected: boolean, + selectedProducts: SearchProducts_products_edges_node[], + setSelectedProducts: (data: SearchProducts_products_edges_node[]) => void +) { + if (isSelected) { + setSelectedProducts( + selectedProducts.filter( + selectedProduct => selectedProduct.id !== product.id + ) + ); + } else { + setSelectedProducts([...selectedProducts, product]); + } } -const initialForm: FormData = { - products: [], - query: "" -}; const AssignProductDialog = withStyles(styles, { name: "AssignProductDialog" })( @@ -71,106 +87,102 @@ const AssignProductDialog = withStyles(styles, { onClose, onFetch, onSubmit - }: AssignProductDialogProps) => ( - -
- {({ data, change }) => ( - <> - {i18n.t("Assign Product")} - - change(event, () => onFetch(data.query))} - label={i18n.t("Search Products", { - context: "product search input label" - })} - placeholder={i18n.t( - "Search by product name, attribute, product type etc...", - { - context: "product search input placeholder" - } - )} - fullWidth - InputProps={{ - autoComplete: "off", - endAdornment: loading && - }} - /> - - - - {products && - products.map(product => { - const isChecked = !!data.products.find( - selectedProduct => selectedProduct.id === product.id - ); + }: AssignProductDialogProps & WithStyles) => { + const [query, onQueryChange] = useSearchQuery(onFetch); + const [selectedProducts, setSelectedProducts] = React.useState< + SearchProducts_products_edges_node[] + >([]); - return ( - - product.thumbnail.url)} + const handleSubmit = () => onSubmit(selectedProducts); + + return ( + + {i18n.t("Assign Product")} + + + }} + /> + +
+
+ + {products && + products.map(product => { + const isSelected = selectedProducts.some( + selectedProduct => selectedProduct.id === product.id + ); + + return ( + + product.thumbnail.url)} + /> + + {product.name} + + + + handleProductAssign( + product, + isSelected, + selectedProducts, + setSelectedProducts + ) + } /> - - {product.name} - - - - isChecked - ? change({ - target: { - name: "products", - value: data.products.filter( - selectedProduct => - selectedProduct.id !== product.id - ) - } - } as any) - : change({ - target: { - name: "products", - value: [...data.products, product] - } - } as any) - } - /> - - - ); - })} - -
-
- - - - {i18n.t("Assign products", { context: "button" })} - - - - )} -
-
- ) + + + ); + })} + + + + + + + + {i18n.t("Assign products", { context: "button" })} + + + + ); + } ); AssignProductDialog.displayName = "AssignProductDialog"; export default AssignProductDialog; diff --git a/src/components/CardTitle/CardTitle.tsx b/src/components/CardTitle/CardTitle.tsx index a800a752b..55b92178d 100644 --- a/src/components/CardTitle/CardTitle.tsx +++ b/src/components/CardTitle/CardTitle.tsx @@ -5,7 +5,7 @@ import { WithStyles } from "@material-ui/core/styles"; import Typography from "@material-ui/core/Typography"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; const styles = (theme: Theme) => @@ -32,7 +32,7 @@ const styles = (theme: Theme) => lineHeight: 1 }, toolbar: { - marginRight: -theme.spacing.unit * 2 + marginRight: -theme.spacing.unit } }); diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index 2a715c8e1..31c23d9ce 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -9,7 +9,6 @@ import { } from "@material-ui/core/styles"; import classNames from "classnames"; import React from "react"; -import { stopPropagation } from "../../misc"; export type CheckboxProps = Omit< MuiCheckboxProps, @@ -19,7 +18,9 @@ export type CheckboxProps = Omit< | "indeterminateIcon" | "classes" | "onChange" + | "onClick" > & { + disableClickPropagation?: boolean; onChange?: (event: React.ChangeEvent) => void; }; @@ -45,7 +46,6 @@ const styles = (theme: Theme) => }, "&:before": { background: "rgba(0, 0, 0, 0)", - borderRadius: 2, content: '""', height: 8, left: 2, @@ -56,7 +56,6 @@ const styles = (theme: Theme) => }, WebkitAppearance: "none", border: `1px solid ${theme.palette.grey[500]}`, - borderRadius: 4, boxSizing: "border-box", cursor: "pointer", height: 14, @@ -84,21 +83,31 @@ const Checkbox = withStyles(styles, { name: "Checkbox" })( className, classes, disabled, + disableClickPropagation, indeterminate, onChange, - onClick, value, name, ...props }: CheckboxProps & WithStyles) => { const inputRef = React.useRef(null); + const handleClick = React.useCallback( + disableClickPropagation + ? event => { + event.stopPropagation(); + inputRef.current.click(); + } + : () => inputRef.current.click(), + [] + ); + return ( inputRef.current.click())} + onClick={handleClick} > diff --git a/src/components/ControlledCheckbox.tsx b/src/components/ControlledCheckbox.tsx index 8ce485c10..aef9e52e2 100644 --- a/src/components/ControlledCheckbox.tsx +++ b/src/components/ControlledCheckbox.tsx @@ -20,7 +20,7 @@ export const ControlledCheckbox: React.StatelessComponent< onChange({ target: { name, value: !checked } })} + disableClickPropagation onChange={() => onChange({ target: { name, value: !checked } })} /> } diff --git a/src/components/ControlledSwitch.tsx b/src/components/ControlledSwitch.tsx index 87b308ba2..07a244006 100644 --- a/src/components/ControlledSwitch.tsx +++ b/src/components/ControlledSwitch.tsx @@ -7,6 +7,9 @@ const styles = (theme: Theme) => createStyles({ label: { marginLeft: theme.spacing.unit * 2 + }, + labelText: { + fontSize: 14 } }); @@ -15,7 +18,7 @@ interface ControlledSwitchProps extends WithStyles { disabled?: boolean; label: string | React.ReactNode; name: string; - secondLabel?: string; + secondLabel?: string | React.ReactNode; uncheckedLabel?: string | React.ReactNode; onChange?(event: React.ChangeEvent); } @@ -46,7 +49,17 @@ export const ControlledSwitch = withStyles(styles, { } label={
- {uncheckedLabel ? (checked ? label : uncheckedLabel) : label} + {uncheckedLabel ? ( + checked ? ( + label + ) : ( + uncheckedLabel + ) + ) : typeof label === "string" ? ( + {label} + ) : ( + label + )}
{secondLabel ? secondLabel : null}
} diff --git a/src/components/Date/Date.test.tsx b/src/components/Date/Date.test.tsx index 738b89fbf..77f2d4126 100644 --- a/src/components/Date/Date.test.tsx +++ b/src/components/Date/Date.test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import * as renderer from "react-test-renderer"; +import renderer from "react-test-renderer"; import { TimezoneProvider } from "../Timezone"; import Date from "./Date"; diff --git a/src/components/Date/Date.tsx b/src/components/Date/Date.tsx index a50a4e909..6a13ae3bb 100644 --- a/src/components/Date/Date.tsx +++ b/src/components/Date/Date.tsx @@ -1,5 +1,5 @@ import Tooltip from "@material-ui/core/Tooltip"; -import * as moment from "moment-timezone"; +import moment from "moment-timezone"; import React from "react"; import useDateLocalize from "@saleor/hooks/useDateLocalize"; diff --git a/src/components/Date/DateTime.tsx b/src/components/Date/DateTime.tsx index f9c24220b..31af9987e 100644 --- a/src/components/Date/DateTime.tsx +++ b/src/components/Date/DateTime.tsx @@ -1,5 +1,5 @@ import Tooltip from "@material-ui/core/Tooltip"; -import * as moment from "moment-timezone"; +import moment from "moment-timezone"; import React from "react"; import ReactMoment from "react-moment"; diff --git a/src/components/DraftRenderer.tsx b/src/components/DraftRenderer.tsx index 052040aa3..67affe033 100644 --- a/src/components/DraftRenderer.tsx +++ b/src/components/DraftRenderer.tsx @@ -1,5 +1,5 @@ import { RawDraftContentState } from "draft-js"; -import * as draftToHtml from "draftjs-to-html"; +import draftToHtml from "draftjs-to-html"; import React from "react"; interface DraftRendererProps { diff --git a/src/components/Dropzone.tsx b/src/components/Dropzone.tsx index 1a8c62114..804532431 100644 --- a/src/components/Dropzone.tsx +++ b/src/components/Dropzone.tsx @@ -1,3 +1,3 @@ /* tslint:disable:no-submodule-imports */ -import * as Dropzone from "react-dropzone/dist/index"; +import Dropzone from "react-dropzone/dist/index"; export default Dropzone; diff --git a/src/components/EditableTableCell/EditableTableCell.tsx b/src/components/EditableTableCell/EditableTableCell.tsx index 17db49a77..594c244ca 100644 --- a/src/components/EditableTableCell/EditableTableCell.tsx +++ b/src/components/EditableTableCell/EditableTableCell.tsx @@ -9,10 +9,10 @@ import { import TableCell from "@material-ui/core/TableCell"; import TextField, { TextFieldProps } from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; -import Form from "@saleor/components/Form"; +import useForm from "@saleor/hooks/useForm"; const styles = (theme: Theme) => createStyles({ @@ -66,48 +66,39 @@ export const EditableTableCell = withStyles(styles, { value, onConfirm }: EditableTableCellProps) => { - const [opened, setOpenStatus] = React.useState(focused); - const enable = () => setOpenStatus(true); - const disable = () => setOpenStatus(false); - const handleConfirm = (data: { value: string }) => { disable(); onConfirm(data.value); }; + const [opened, setOpenStatus] = React.useState(focused); + const { change, data } = useForm({ value }, [], handleConfirm); + const enable = () => setOpenStatus(true); + const disable = () => setOpenStatus(false); + return ( {opened &&
} -
- {({ change, data }) => ( - <> - - {value || defaultValue} - - {opened && ( -
- - - - - -
- )} - - )} -
+ + {value || defaultValue} + + {opened && ( +
+ + + + + +
+ )} ); } diff --git a/src/components/ErrorPage/ErrorPage.tsx b/src/components/ErrorPage/ErrorPage.tsx index f6046e2f0..485cc22d9 100644 --- a/src/components/ErrorPage/ErrorPage.tsx +++ b/src/components/ErrorPage/ErrorPage.tsx @@ -9,7 +9,7 @@ import Typography from "@material-ui/core/Typography"; import React from "react"; import SVG from "react-inlinesvg"; -import * as notFoundImage from "@assets/images/what.svg"; +import notFoundImage from "@assets/images/what.svg"; import i18n from "../../i18n"; export interface ErrorPageProps extends WithStyles { diff --git a/src/components/ExtendedPageHeader/ExtendedPageHeader.tsx b/src/components/ExtendedPageHeader/ExtendedPageHeader.tsx index c70ead68d..7a71d6375 100644 --- a/src/components/ExtendedPageHeader/ExtendedPageHeader.tsx +++ b/src/components/ExtendedPageHeader/ExtendedPageHeader.tsx @@ -4,7 +4,7 @@ import { withStyles, WithStyles } from "@material-ui/core/styles"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; const styles = (theme: Theme) => diff --git a/src/components/Filter/Filter.tsx b/src/components/Filter/Filter.tsx index fbf862ad3..fa32d49ae 100644 --- a/src/components/Filter/Filter.tsx +++ b/src/components/Filter/Filter.tsx @@ -11,7 +11,7 @@ import { import { fade } from "@material-ui/core/styles/colorManipulator"; import Typography from "@material-ui/core/Typography"; import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import { FilterContent } from "."; diff --git a/src/components/Filter/FilterContent.tsx b/src/components/Filter/FilterContent.tsx index 7df9ce86c..40b798327 100644 --- a/src/components/Filter/FilterContent.tsx +++ b/src/components/Filter/FilterContent.tsx @@ -47,7 +47,7 @@ const FilterContent: React.FC = ({ }) => { const [menuValue, setMenuValue] = React.useState(""); const [filterValue, setFilterValue] = React.useState(""); - const classes = useStyles(); + const classes = useStyles({}); const activeMenu = menuValue ? getMenuItemByValue(filters, menuValue) diff --git a/src/components/Filter/FilterElement.tsx b/src/components/Filter/FilterElement.tsx index 202aab160..e9735d5a0 100644 --- a/src/components/Filter/FilterElement.tsx +++ b/src/components/Filter/FilterElement.tsx @@ -41,7 +41,7 @@ const FilterElement: React.FC = ({ onChange, value }) => { - const classes = useStyles(); + const classes = useStyles({}); if (filter.data.type === FieldType.date) { return ( diff --git a/src/components/FilterBar/FilterBar.tsx b/src/components/FilterBar/FilterBar.tsx index db92026cc..f5adc696d 100644 --- a/src/components/FilterBar/FilterBar.tsx +++ b/src/components/FilterBar/FilterBar.tsx @@ -41,6 +41,7 @@ const FilterBar: React.FC = ({ onTabChange(tabIndex + 1)} label={tab.name} + key={tabIndex} /> ))} {isCustom && ( diff --git a/src/components/Form/Form.tsx b/src/components/Form/Form.tsx index 306348168..25512cf57 100644 --- a/src/components/Form/Form.tsx +++ b/src/components/Form/Form.tsx @@ -1,170 +1,42 @@ import React from "react"; -import { UserError } from "../../types"; -export interface FormProps { - children: (props: { - data: T; - hasChanged: boolean; - errors: { [key: string]: string }; - change(event: React.ChangeEvent, cb?: () => void); - reset(); - submit(event?: React.FormEvent); - }) => React.ReactElement; +import useForm, { UseFormResult } from "@saleor/hooks/useForm"; +import { UserError } from "@saleor/types"; + +export interface FormProps { + children: (props: UseFormResult) => React.ReactNode; + confirmLeave?: boolean; errors?: UserError[]; initial?: T; - confirmLeave?: boolean; - useForm?: boolean; resetOnSubmit?: boolean; - onSubmit?(data: T); + onSubmit?: (data: T) => void; } -interface FormComponentProps extends FormProps { - hasChanged: boolean; - toggleFormChangeState: () => void; -} +function Form(props: FormProps) { + const { children, errors, initial, resetOnSubmit, onSubmit } = props; + const renderProps = useForm(initial, errors, onSubmit); -interface FormState { - initial: T; - fields: T; - hasChanged: boolean; -} + function handleSubmit(event?: React.FormEvent, cb?: () => void) { + const { reset, submit } = renderProps; -class FormComponent extends React.Component< - FormComponentProps, - FormState -> { - static getDerivedStateFromProps( - nextProps: FormComponentProps, - prevState: FormState - ): FormState { - const changedFields = Object.keys(nextProps.initial).filter( - nextFieldName => - JSON.stringify(nextProps.initial[nextFieldName]) !== - JSON.stringify(prevState.initial[nextFieldName]) - ); - if (changedFields.length > 0) { - const swapFields = changedFields.reduce((prev, curr) => { - prev[curr] = nextProps.initial[curr]; - return prev; - }, {}); - - return { - fields: { - ...(prevState.fields as any), - ...swapFields - }, - hasChanged: false, - initial: { - ...(prevState.initial as any), - ...swapFields - } - }; - } - return null; - } - - state: FormState = { - fields: this.props.initial, - hasChanged: false, - initial: this.props.initial - }; - - componentDidUpdate() { - const { hasChanged, confirmLeave, toggleFormChangeState } = this.props; - if (this.state.hasChanged !== hasChanged && confirmLeave) { - toggleFormChangeState(); - } - } - - componentDidMount() { - const { hasChanged, confirmLeave, toggleFormChangeState } = this.props; - if (this.state.hasChanged !== hasChanged && confirmLeave) { - toggleFormChangeState(); - } - } - - componentWillUnmount() { - const { hasChanged, confirmLeave, toggleFormChangeState } = this.props; - if (hasChanged && confirmLeave) { - toggleFormChangeState(); - } - } - - handleChange = (event: React.ChangeEvent, cb?: () => void) => { - const { target } = event; - if (!(target.name in this.state.fields)) { - console.error(`Unknown form field: ${target.name}`); - return; - } - - this.setState( - { - fields: { - ...(this.state.fields as any), - [target.name]: target.value - }, - hasChanged: true - }, - typeof cb === "function" ? cb : undefined - ); - }; - - handleKeyDown = (event: React.KeyboardEvent) => { - switch (event.keyCode) { - // Enter - case 13: - this.props.onSubmit(this.state.fields); - break; - } - }; - - handleSubmit = (event?: React.FormEvent, cb?: () => void) => { - const { resetOnSubmit, onSubmit } = this.props; if (event) { event.stopPropagation(); event.preventDefault(); } - if (onSubmit !== undefined) { - onSubmit(this.state.fields); - } + if (cb) { cb(); } + if (resetOnSubmit) { - this.setState({ - fields: this.state.initial - }); + reset(); } - }; - render() { - const { children, errors, useForm = true } = this.props; - - const contents = children({ - change: this.handleChange, - data: this.state.fields, - errors: errors - ? errors.reduce( - (prev, curr) => ({ - ...prev, - [curr.field.split(":")[0]]: curr.message - }), - {} - ) - : {}, - hasChanged: this.state.hasChanged, - reset: () => - this.setState({ - fields: this.state.initial - }), - submit: this.handleSubmit - }); - - return useForm ? ( -
{contents}
- ) : ( -
{contents}
- ); + submit(); } + + return
{children(renderProps)}
; } -export default FormComponent; +Form.displayName = "Form"; + +export default Form; diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts index 1f0b7dda4..98ebc8cc6 100644 --- a/src/components/Form/index.ts +++ b/src/components/Form/index.ts @@ -1,5 +1,4 @@ export * from "./Form"; +export { default } from "./Form"; export { default as FormActions } from "./FormActions"; export * from "./FormActions"; -export * from "./FormContext"; -export { default } from "./FormContext"; diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 11f5a79e8..762cd74a8 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -5,7 +5,7 @@ import { WithStyles } from "@material-ui/core/styles"; import Typography, { TypographyProps } from "@material-ui/core/Typography"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; const styles = (theme: Theme) => diff --git a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx index 0c177fc5e..7c6ac5d97 100644 --- a/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx +++ b/src/components/MultiAutocompleteSelectField/MultiAutocompleteSelectField.tsx @@ -1,4 +1,5 @@ import CircularProgress from "@material-ui/core/CircularProgress"; +import IconButton from "@material-ui/core/IconButton"; import MenuItem from "@material-ui/core/MenuItem"; import Paper from "@material-ui/core/Paper"; import { @@ -8,27 +9,77 @@ import { WithStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import CloseIcon from "@material-ui/icons/Close"; import Downshift, { ControllerStateAndHelpers } from "downshift"; import React from "react"; +import { compareTwoStrings } from "string-similarity"; -import i18n from "../../i18n"; -import ArrowDropdownIcon from "../../icons/ArrowDropdown"; -import Debounce, { DebounceProps } from "../Debounce"; +import { fade } from "@material-ui/core/styles/colorManipulator"; +import Checkbox from "@saleor/components/Checkbox"; +import Debounce, { DebounceProps } from "@saleor/components/Debounce"; +import i18n from "@saleor/i18n"; +import ArrowDropdownIcon from "@saleor/icons/ArrowDropdown"; +import Hr from "../Hr"; -interface ChoiceType { +export interface MultiAutocompleteChoiceType { label: string; value: string; } const styles = (theme: Theme) => createStyles({ + checkbox: { + height: 24, + width: 20 + }, chip: { - margin: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px` + width: "100%" + }, + chipClose: { + height: 32, + padding: 0, + width: 32 + }, + chipContainer: { + display: "flex", + flexDirection: "column", + marginTop: theme.spacing.unit + }, + chipInner: { + "& svg": { + color: theme.palette.primary.contrastText + }, + alignItems: "center", + background: fade(theme.palette.primary.main, 0.6), + borderRadius: 24, + color: theme.palette.primary.contrastText, + display: "flex", + justifyContent: "space-between", + margin: `${theme.spacing.unit}px 0`, + paddingLeft: theme.spacing.unit * 2, + paddingRight: theme.spacing.unit + }, + chipLabel: { + color: theme.palette.primary.contrastText }, container: { flexGrow: 1, position: "relative" }, + hr: { + margin: `${theme.spacing.unit}px 0` + }, + menuItem: { + display: "grid", + gridColumnGap: theme.spacing.unit + "px", + gridTemplateColumns: "20px 1fr", + height: "auto", + whiteSpace: "normal" + }, + menuItemLabel: { + overflowWrap: "break-word" + }, paper: { left: 0, marginTop: theme.spacing.unit, @@ -39,39 +90,32 @@ const styles = (theme: Theme) => } }); -export interface MultiAutocompleteSelectFieldChildrenFunc { - deleteItem: (item: ChoiceType) => void; - items: ChoiceType[]; -} -export type MultiAutocompleteSelectFieldChildren = ( - props: MultiAutocompleteSelectFieldChildrenFunc -) => React.ReactNode; - -export interface MultiAutocompleteSelectFieldProps - extends WithStyles { +export interface MultiAutocompleteSelectFieldProps { + allowCustomValues?: boolean; + displayValues: MultiAutocompleteChoiceType[]; name: string; - children: MultiAutocompleteSelectFieldChildren; - choices: ChoiceType[]; - value?: ChoiceType[]; + choices: MultiAutocompleteChoiceType[]; + value: string[]; loading?: boolean; placeholder?: string; helperText?: string; label?: string; - fetchChoices(value: string); - onChange(event); + fetchChoices?: (value: string) => void; + onChange: (event: React.ChangeEvent) => void; } const DebounceAutocomplete: React.ComponentType< DebounceProps > = Debounce; -export const MultiAutocompleteSelectField = withStyles(styles, { +export const MultiAutocompleteSelectFieldComponent = withStyles(styles, { name: "MultiAutocompleteSelectField" })( ({ - children, + allowCustomValues, choices, classes, + displayValues, helperText, label, loading, @@ -80,102 +124,200 @@ export const MultiAutocompleteSelectField = withStyles(styles, { value, fetchChoices, onChange - }: MultiAutocompleteSelectFieldProps) => { + }: MultiAutocompleteSelectFieldProps & WithStyles) => { const handleSelect = ( - item: ChoiceType, - { reset }: ControllerStateAndHelpers + item: string, + downshiftOpts?: ControllerStateAndHelpers ) => { - reset({ inputValue: "" }); - onChange({ target: { name, value: [...value, item] } }); + if (downshiftOpts) { + downshiftOpts.reset({ inputValue: "" }); + } + onChange({ + target: { name, value: item } + } as any); }; - const handleDelete = (item: ChoiceType) => { - const newValue = value.slice(); - newValue.splice( - value.findIndex(listItem => listItem.value === item.value), - 1 - ); - onChange({ target: { name, value: newValue } }); - }; - - const filteredChoices = choices.filter( - suggestion => value.map(v => v.value).indexOf(suggestion.value) === -1 - ); + const suggestions = choices.filter(choice => !value.includes(choice.value)); return ( - - {debounce => ( - (item ? item.label : "")} - onSelect={handleSelect} - onInputValueChange={value => debounce(value)} - > - {({ - getInputProps, - getItemProps, - isOpen, - selectedItem, - toggleMenu, - closeMenu, - openMenu, - highlightedIndex - }) => { - return ( -
- - {loading ? ( - - ) : ( - - )} -
- ), - id: undefined, - onBlur: closeMenu, - onFocus: openMenu - }} - helperText={helperText} - label={label} - fullWidth={true} - /> - {isOpen && ( - - {!loading && filteredChoices.length > 0 - ? filteredChoices.map((suggestion, index) => ( - - {suggestion.label} - - )) - : !loading && ( - - {i18n.t("No results found")} - - )} - + <> + ""} + > + {({ + getInputProps, + getItemProps, + isOpen, + toggleMenu, + highlightedIndex, + inputValue + }) => ( +
+ + {loading ? ( + + ) : ( + + )} +
+ ), + id: undefined, + onClick: toggleMenu + }} + helperText={helperText} + label={label} + fullWidth={true} + /> + {isOpen && (!!inputValue || !!choices.length) && ( + + {choices.length > 0 || + displayValues.length > 0 || + allowCustomValues ? ( + <> + {displayValues.map(value => ( + + + + {value.label} + + + ))} + {displayValues.length > 0 && suggestions.length > 0 && ( +
+ )} + {suggestions.map((suggestion, index) => ( + + + + {suggestion.label} + + + ))} + {allowCustomValues && + inputValue && + !choices.find( + choice => + choice.label.toLowerCase() === + inputValue.toLowerCase() + ) && ( + + + {i18n.t("Add new value: {{ value }}", { + context: "add custom option", + value: inputValue + })} + + + )} + + ) : ( + !loading && ( + + {i18n.t("No results found")} + + ) )} - {children({ - deleteItem: handleDelete, - items: selectedItem - })} -
- ); - }} - + + )} + + )} + +
+ {displayValues.map(value => ( +
+
+ + {value.label} + + handleSelect(value.value)} + > + + +
+
+ ))} +
+ + ); + } +); +const MultiAutocompleteSelectField: React.FC< + MultiAutocompleteSelectFieldProps +> = ({ choices, fetchChoices, ...props }) => { + const [query, setQuery] = React.useState(""); + if (fetchChoices) { + return ( + + {debounceFn => ( + )} ); } -); + + const sortedChoices = choices.sort((a, b) => { + const ratingA = compareTwoStrings(query, a.label); + const ratingB = compareTwoStrings(query, b.label); + if (ratingA > ratingB) { + return -1; + } + if (ratingA < ratingB) { + return 1; + } + return 0; + }); + + return ( + setQuery(q || "")} + choices={sortedChoices} + {...props} + /> + ); +}; MultiAutocompleteSelectField.displayName = "MultiAutocompleteSelectField"; export default MultiAutocompleteSelectField; diff --git a/src/components/NotFoundPage/NotFoundPage.tsx b/src/components/NotFoundPage/NotFoundPage.tsx index 73064000a..236e905a0 100644 --- a/src/components/NotFoundPage/NotFoundPage.tsx +++ b/src/components/NotFoundPage/NotFoundPage.tsx @@ -9,8 +9,8 @@ import Typography from "@material-ui/core/Typography"; import React from "react"; import SVG from "react-inlinesvg"; -import * as notFoundImage from "@assets/images/not-found-404.svg"; -import i18n from "../../i18n"; +import notFoundImage from "@assets/images/not-found-404.svg"; +import i18n from "@saleor/i18n"; const styles = (theme: Theme) => createStyles({ diff --git a/src/components/PageHeader/PageHeader.tsx b/src/components/PageHeader/PageHeader.tsx index 1fc53ec1c..843be5df3 100644 --- a/src/components/PageHeader/PageHeader.tsx +++ b/src/components/PageHeader/PageHeader.tsx @@ -12,6 +12,9 @@ import Skeleton from "../Skeleton"; const styles = (theme: Theme) => createStyles({ + root: { + display: "flex" + }, title: { flex: 1, fontSize: 24, @@ -35,7 +38,7 @@ const PageHeader = withStyles(styles)( } > - {children} +
{children}
) ); diff --git a/src/components/ProductList/ProductList.tsx b/src/components/ProductList/ProductList.tsx index ccbe45ffe..4aa0b39da 100644 --- a/src/components/ProductList/ProductList.tsx +++ b/src/components/ProductList/ProductList.tsx @@ -11,11 +11,15 @@ import TableFooter from "@material-ui/core/TableFooter"; import TableRow from "@material-ui/core/TableRow"; import React from "react"; -import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import TableCellAvatar, { + AVATAR_MARGIN +} from "@saleor/components/TableCellAvatar"; +import { ProductListColumns } from "@saleor/config"; +import i18n from "@saleor/i18n"; +import { maybe, renderCollection } from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; +import { isSelected } from "@saleor/utils/lists"; import { CategoryDetails_category_products_edges_node } from "../../categories/types/CategoryDetails"; -import i18n from "../../i18n"; -import { maybe, renderCollection } from "../../misc"; -import { ListActions, ListProps } from "../../types"; import Checkbox from "../Checkbox"; import Money from "../Money"; import Skeleton from "../Skeleton"; @@ -27,7 +31,7 @@ const styles = (theme: Theme) => createStyles({ [theme.breakpoints.up("lg")]: { colName: { - width: 430 + width: "auto" }, colPrice: { width: 200 @@ -39,12 +43,14 @@ const styles = (theme: Theme) => width: 200 } }, - avatarCell: { - paddingLeft: theme.spacing.unit * 2, - paddingRight: 0, - width: theme.spacing.unit * 5 + colFill: { + padding: 0, + width: "100%" }, colName: {}, + colNameHeader: { + marginLeft: AVATAR_MARGIN + }, colPrice: { textAlign: "right" }, @@ -53,6 +59,12 @@ const styles = (theme: Theme) => link: { cursor: "pointer" }, + table: { + tableLayout: "fixed" + }, + tableContainer: { + overflowX: "scroll" + }, textLeft: { textAlign: "left" }, @@ -62,7 +74,7 @@ const styles = (theme: Theme) => }); interface ProductListProps - extends ListProps, + extends ListProps, ListActions, WithStyles { products: CategoryDetails_category_products_edges_node[]; @@ -71,6 +83,7 @@ interface ProductListProps export const ProductList = withStyles(styles, { name: "ProductList" })( ({ classes, + settings, disabled, isChecked, pageInfo, @@ -81,114 +94,158 @@ export const ProductList = withStyles(styles, { name: "ProductList" })( toolbar, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick - }: ProductListProps) => ( - - - - - {i18n.t("Name", { context: "object" })} - - - {i18n.t("Type", { context: "object" })} - - - {i18n.t("Published", { context: "object" })} - - - {i18n.t("Price", { context: "object" })} - - - - - - - - - {renderCollection( - products, - product => { - const isSelected = product ? isChecked(product.id) : false; + }: ProductListProps) => { + const displayColumn = React.useCallback( + (column: ProductListColumns) => + isSelected(column, settings.columns, (a, b) => a === b), + [settings.columns] + ); + const numberOfColumns = 2 + settings.columns.length; - return ( - - - toggle(product.id)} - /> - - product.thumbnail.url)} - /> - - {product ? product.name : } - - - {product && product.productType ? ( - product.productType.name - ) : ( - - )} - - - {product && maybe(() => product.isAvailable !== undefined) ? ( - - ) : ( - - )} - - - {maybe(() => product.basePrice) && - maybe(() => product.basePrice.amount) !== undefined && - maybe(() => product.basePrice.currency) !== undefined ? ( - - ) : ( - - )} - - - ); - }, - () => ( + return ( +
+
+ + + {displayColumn("productType") && } + {displayColumn("isPublished") && ( + + )} + {displayColumn("price") && } + + + + {i18n.t("Name", { context: "object" })} + + + {displayColumn("productType") && ( + + {i18n.t("Type", { context: "object" })} + + )} + {displayColumn("isPublished") && ( + + {i18n.t("Published", { context: "object" })} + + )} + {displayColumn("price") && ( + + {i18n.t("Price", { context: "object" })} + + )} + + - {i18n.t("No products found")} + - ) - )} - -
- ) + + + {renderCollection( + products, + product => { + const isSelected = product ? isChecked(product.id) : false; + + return ( + + + toggle(product.id)} + /> + + product.thumbnail.url)} + > + {product ? product.name : } + + {displayColumn("productType") && ( + + {product && product.productType ? ( + product.productType.name + ) : ( + + )} + + )} + {displayColumn("isPublished") && ( + + {product && + maybe(() => product.isAvailable !== undefined) ? ( + + ) : ( + + )} + + )} + {displayColumn("price") && ( + + {maybe(() => product.basePrice) && + maybe(() => product.basePrice.amount) !== undefined && + maybe(() => product.basePrice.currency) !== + undefined ? ( + + ) : ( + + )} + + )} + + ); + }, + () => ( + + + {i18n.t("No products found")} + + + ) + )} + + + + ); + } ); ProductList.displayName = "ProductList"; export default ProductList; diff --git a/src/components/RichTextEditor/RichTextEditor.tsx b/src/components/RichTextEditor/RichTextEditor.tsx index f5718e513..6c4a7e01c 100644 --- a/src/components/RichTextEditor/RichTextEditor.tsx +++ b/src/components/RichTextEditor/RichTextEditor.tsx @@ -6,7 +6,7 @@ import { } from "@material-ui/core/styles"; import { fade } from "@material-ui/core/styles/colorManipulator"; import Typography from "@material-ui/core/Typography"; -import * as classNames from "classnames"; +import classNames from "classnames"; import { RawDraftContentState } from "draft-js"; import { BLOCK_TYPE, @@ -14,6 +14,7 @@ import { ENTITY_TYPE, INLINE_STYLE } from "draftail"; +import isEqual from "lodash-es/isEqual"; import React from "react"; import BoldIcon from "../../icons/BoldIcon"; @@ -29,6 +30,7 @@ import UnorderedListIcon from "../../icons/UnorderedListIcon"; // import ImageEntity from "./ImageEntity"; // import ImageSource from "./ImageSource"; +import { ChangeEvent } from "@saleor/hooks/useForm"; import LinkEntity from "./LinkEntity"; import LinkSource from "./LinkSource"; @@ -45,22 +47,6 @@ export interface RichTextEditorProps { const styles = (theme: Theme) => createStyles({ - "@keyframes focus": { - from: { - transform: "scaleX(0) scaleY(1)" - }, - to: { - transform: "scaleX(1) scaleY(1)" - } - }, - "@keyframes hover": { - from: { - transform: "scaleX(1) scaleY(0)" - }, - to: { - transform: "scaleX(1) scaleY(1)" - } - }, error: { color: theme.palette.error.main }, @@ -68,21 +54,13 @@ const styles = (theme: Theme) => marginTop: theme.spacing.unit * 0.75 }, input: { - "&:hover": { - borderBottomColor: theme.palette.primary.main - }, - backgroundColor: theme.overrides.MuiFilledInput.root.backgroundColor, - borderBottom: `1px rgba(0, 0, 0, 0) solid`, - borderTopLeftRadius: 4, - borderTopRightRadius: 4, - padding: "27px 12px 10px", - position: "relative", - transition: theme.transitions.duration.shortest + "ms" + position: "relative" }, label: { fontSize: theme.typography.caption.fontSize, - marginBottom: theme.spacing.unit * 2, - marginTop: -21 + left: 12, + position: "absolute", + top: 9 }, linkIcon: { marginTop: 2 @@ -97,8 +75,6 @@ const styles = (theme: Theme) => color: theme.palette.primary.light }, "&:after": { - animationDuration: theme.transitions.duration.shortest + "ms", - animationFillMode: "both", background: theme.palette.getContrastText( theme.palette.background.default ), @@ -120,16 +96,19 @@ const styles = (theme: Theme) => "& .Draftail": { "&-Editor": { "&--focus": { - "& .DraftEditor": { - "&-editorContainer": { - "&:after": { - animationName: "focus !important", - background: theme.palette.primary.main, - transform: "scaleX(0) scaleY(1)" - } - } - } - } + boxShadow: `inset 0px 0px 0px 2px ${theme.palette.primary.main}` + }, + "&:hover": { + borderColor: theme.palette.primary.main + }, + border: `1px ${ + theme.overrides.MuiOutlinedInput.root.borderColor + } solid`, + borderTopLeftRadius: 4, + borderTopRightRadius: 4, + padding: "27px 12px 10px", + position: "relative", + transition: theme.transitions.duration.shortest + "ms" }, "&-Toolbar": { "&Button": { @@ -190,6 +169,7 @@ const styles = (theme: Theme) => display: "inline-flex", flexWrap: "wrap", marginBottom: theme.spacing.unit, + marginTop: 10, [theme.breakpoints.down(460)]: { width: "min-content" } @@ -205,24 +185,7 @@ const styles = (theme: Theme) => "&$error": { "& .Draftail": { "&-Editor": { - "& .DraftEditor": { - "&-editorContainer": { - "&:after": { - animationName: "none", - background: theme.palette.error.main, - transform: "scaleX(1) scaleY(1)" - } - } - }, - "&--focus": { - "& .DraftEditor": { - "&-editorContainer": { - "&:after": { - animationName: "none !important" - } - } - } - } + borderColor: theme.palette.error.main } } } @@ -240,6 +203,23 @@ const styles = (theme: Theme) => marginLeft: 10 } }); + +function handleSave( + value: any, + initial: any, + name: string, + onChange: (event: ChangeEvent) => void +) { + if (value && !isEqual(value, initial)) { + onChange({ + target: { + name, + value + } + }); + } +} + const RichTextEditor = withStyles(styles, { name: "RichTextEditor" })( ({ classes, @@ -267,14 +247,7 @@ const RichTextEditor = withStyles(styles, { name: "RichTextEditor" })( rawContentState={ initial && Object.keys(initial).length > 0 ? initial : null } - onSave={value => - onChange({ - target: { - name, - value - } - } as any) - } + onSave={value => handleSave(value, initial, name, onChange)} blockTypes={[ { icon: , diff --git a/src/components/SaveButtonBar/SaveButtonBar.tsx b/src/components/SaveButtonBar/SaveButtonBar.tsx index 8ed381f20..7c1877546 100644 --- a/src/components/SaveButtonBar/SaveButtonBar.tsx +++ b/src/components/SaveButtonBar/SaveButtonBar.tsx @@ -9,7 +9,7 @@ import { import classNames from "classnames"; import React from "react"; -import useScroll from "@saleor/hooks/useScroll"; +import useWindowScroll from "@saleor/hooks/useWindowScroll"; import i18n from "../../i18n"; import { maybe } from "../../misc"; import AppActionContext from "../AppLayout/AppActionContext"; @@ -82,7 +82,7 @@ export const SaveButtonBar = withStyles(styles, { name: "SaveButtonBar" })( onSave, ...props }: SaveButtonBarProps) => { - const scrollPosition = useScroll(); + const scrollPosition = useWindowScroll(); const scrolledToBottom = scrollPosition.y + window.innerHeight >= document.body.scrollHeight; diff --git a/src/components/SeoForm/SeoForm.tsx b/src/components/SeoForm/SeoForm.tsx index 1aceab7c4..fecca3cc1 100644 --- a/src/components/SeoForm/SeoForm.tsx +++ b/src/components/SeoForm/SeoForm.tsx @@ -9,7 +9,7 @@ import { } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import i18n from "../../i18n"; @@ -43,6 +43,9 @@ const styles = (theme: Theme) => flex: 1 }, labelContainer: { + "& span": { + paddingRight: 30 + }, display: "flex" }, preview: { @@ -128,7 +131,6 @@ const SeoForm = withStyles(styles, { name: "SeoForm" })( )} value={title.slice(0, 69)} disabled={loading || disabled} - InputLabelProps={{ shrink: true }} placeholder={titlePlaceholder} onChange={onChange} fullWidth @@ -158,7 +160,6 @@ const SeoForm = withStyles(styles, { name: "SeoForm" })( fullWidth multiline placeholder={descriptionPlaceholder} - InputLabelProps={{ shrink: true }} rows={10} /> diff --git a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx index 74c47c9db..35bab83f6 100644 --- a/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx +++ b/src/components/SingleAutocompleteSelectField/SingleAutocompleteSelectField.tsx @@ -1,4 +1,5 @@ import { Omit } from "@material-ui/core"; +import CircularProgress from "@material-ui/core/CircularProgress"; import { InputProps } from "@material-ui/core/Input"; import MenuItem from "@material-ui/core/MenuItem"; import Paper from "@material-ui/core/Paper"; @@ -9,10 +10,12 @@ import { WithStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; import Downshift from "downshift"; import React from "react"; import { compareTwoStrings } from "string-similarity"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; import i18n from "../../i18n"; import ArrowDropdownIcon from "../../icons/ArrowDropdown"; import Debounce, { DebounceProps } from "../Debounce"; @@ -23,6 +26,10 @@ const styles = (theme: Theme) => flexGrow: 1, position: "relative" }, + menuItem: { + height: "auto", + whiteSpace: "normal" + }, paper: { borderRadius: 4, left: 0, @@ -34,26 +41,26 @@ const styles = (theme: Theme) => } }); +export interface SingleAutocompleteChoiceType { + label: string; + value: any; +} export interface SingleAutocompleteSelectFieldProps { error?: boolean; name: string; - choices: Array<{ - label: string; - value: any; - }>; - value?: { - label: string; - value: any; - }; + displayValue: string; + emptyOption?: boolean; + choices: SingleAutocompleteChoiceType[]; + value?: string; disabled?: boolean; loading?: boolean; placeholder?: string; - custom?: boolean; + allowCustomValues?: boolean; helperText?: string; label?: string; InputProps?: InputProps; - fetchChoices?(value: string); - onChange(event); + fetchChoices?: (value: string) => void; + onChange: (event: React.ChangeEvent) => void; } interface SingleAutocompleteSelectFieldState { @@ -73,8 +80,10 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { ({ choices, classes, - custom, + allowCustomValues, disabled, + displayValue, + emptyOption, error, helperText, label, @@ -86,16 +95,24 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { fetchChoices, onChange }: SingleAutocompleteSelectFieldProps & WithStyles) => { - const handleChange = item => onChange({ target: { name, value: item } }); + const [prevDisplayValue] = useStateFromProps(displayValue); + const handleChange = item => + onChange({ + target: { + name, + value: item + } + } as any); return ( {debounceFn => ( (item ? item.label : "")} - onSelect={handleChange} + defaultInputValue={displayValue} + itemToString={() => displayValue} onInputValueChange={value => debounceFn(value)} + onSelect={handleChange} + selectedItem={value} > {({ getInputProps, @@ -106,13 +123,18 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { toggleMenu, openMenu, closeMenu, - highlightedIndex + highlightedIndex, + reset }) => { - const isCustom = + const isCustomValueSelected = choices && selectedItem - ? choices.filter(c => c.value === selectedItem.value) - .length === 0 + ? choices.filter(c => c.value === selectedItem).length === 0 : false; + + if (prevDisplayValue !== displayValue) { + reset({ inputValue: displayValue }); + } + return (
+
+ {loading ? ( + + ) : ( + + )} +
), error, id: undefined, @@ -136,36 +162,66 @@ const SingleAutocompleteSelectFieldComponent = withStyles(styles, { label={label} fullWidth={true} /> - {isOpen && ( + {isOpen && (!!inputValue || !!choices.length) && ( - {loading ? ( - - {i18n.t("Loading...")} - - ) : choices.length > 0 || custom ? ( + {choices.length > 0 || allowCustomValues ? ( <> - {choices.map((suggestion, index) => ( + {emptyOption && ( - {suggestion.label} - - ))} - {custom && ( - - {i18n.t("Add custom value")} + + {i18n.t("None")} + )} + {choices.map((suggestion, index) => { + const choiceIndex = index + (emptyOption ? 1 : 0); + + return ( + + {suggestion.label} + + ); + })} + {allowCustomValues && + !!inputValue && + !choices.find( + choice => + choice.label.toLowerCase() === + inputValue.toLowerCase() + ) && ( + + {i18n.t("Add new value: {{ value }}", { + context: "add custom option", + value: inputValue + })} + + )} ) : ( diff --git a/src/components/Skeleton.tsx b/src/components/Skeleton.tsx index 5bcefc194..7327f3418 100644 --- a/src/components/Skeleton.tsx +++ b/src/components/Skeleton.tsx @@ -17,6 +17,11 @@ const styles = (theme: Theme) => opacity: 1 } }, + primary: { + "&$skeleton": { + background: theme.palette.primary.main + } + }, skeleton: { animation: "skeleton-animation .75s linear infinite forwards alternate", background: theme.palette.background.default, @@ -29,12 +34,18 @@ const styles = (theme: Theme) => interface SkeletonProps extends WithStyles { className?: string; + primary?: boolean; style?: React.CSSProperties; } const Skeleton = withStyles(styles, { name: "Skeleton" })( - ({ className, classes, style }: SkeletonProps) => ( - + ({ className, classes, primary, style }: SkeletonProps) => ( + ) diff --git a/src/components/StatusLabel/StatusLabel.tsx b/src/components/StatusLabel/StatusLabel.tsx index 4d5f979d2..267dcb89b 100644 --- a/src/components/StatusLabel/StatusLabel.tsx +++ b/src/components/StatusLabel/StatusLabel.tsx @@ -6,7 +6,7 @@ import { WithStyles } from "@material-ui/core/styles"; import Typography, { TypographyProps } from "@material-ui/core/Typography"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; const styles = (theme: Theme) => { diff --git a/src/components/Tab/Tab.tsx b/src/components/Tab/Tab.tsx index 9e7fa1f58..595845b1c 100644 --- a/src/components/Tab/Tab.tsx +++ b/src/components/Tab/Tab.tsx @@ -5,7 +5,7 @@ import { WithStyles } from "@material-ui/core/styles"; import Typography from "@material-ui/core/Typography"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; const styles = (theme: Theme) => diff --git a/src/components/TableCellAvatar/TableCellAvatar.tsx b/src/components/TableCellAvatar/TableCellAvatar.tsx index 29e5d7c73..19ce47f88 100644 --- a/src/components/TableCellAvatar/TableCellAvatar.tsx +++ b/src/components/TableCellAvatar/TableCellAvatar.tsx @@ -7,11 +7,13 @@ import { } from "@material-ui/core/styles"; import TableCell from "@material-ui/core/TableCell"; import Cached from "@material-ui/icons/Cached"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import Image from "../../icons/Image"; +export const AVATAR_MARGIN = 56; + const styles = (theme: Theme) => createStyles({ avatar: { @@ -19,8 +21,17 @@ const styles = (theme: Theme) => border: `1px solid ${theme.overrides.MuiCard.root.borderColor}`, borderRadius: 2, color: "#bdbdbd", + display: "inline-flex", padding: theme.spacing.unit / 2 }, + children: { + alignSelf: "center", + marginLeft: theme.spacing.unit * 2 + }, + content: { + alignItems: "center", + display: "flex" + }, root: { paddingRight: theme.spacing.unit * 3, width: "1%" @@ -31,25 +42,35 @@ interface TableCellAvatarProps extends WithStyles { className?: string; thumbnail?: string; avatarProps?: string; + children?: React.ReactNode | React.ReactNodeArray; } const TableCellAvatar = withStyles(styles, { name: "TableCellAvatar" })( - ({ classes, className, thumbnail, avatarProps }: TableCellAvatarProps) => ( + ({ + classes, + children, + className, + thumbnail, + avatarProps + }: TableCellAvatarProps) => ( - {thumbnail === undefined ? ( - - - - ) : thumbnail === null ? ( - - - - ) : ( - - )} +
+ {thumbnail === undefined ? ( + + + + ) : thumbnail === null ? ( + + + + ) : ( + + )} + {children} +
) ); diff --git a/src/components/TableFilter/FilterChips.tsx b/src/components/TableFilter/FilterChips.tsx index f75a77d83..273829c19 100644 --- a/src/components/TableFilter/FilterChips.tsx +++ b/src/components/TableFilter/FilterChips.tsx @@ -27,7 +27,7 @@ const useInputStyles = makeStyles({ }); const Search: React.FC = props => { - const classes = useInputStyles(); + const classes = useInputStyles({}); return ( }, checkboxPartialSelect: { "&:after": { - background: "#fff", + background: theme.palette.common.white, content: "''", height: 2, position: "absolute", @@ -50,6 +52,10 @@ const styles = (theme: Theme) => height: 47, marginRight: -theme.spacing.unit * 2 }, + dragRows: { + padding: 0, + width: 52 + }, padding: { "&:last-child": { padding: 0 @@ -57,7 +63,6 @@ const styles = (theme: Theme) => }, root: { backgroundColor: fade(theme.palette.primary.main, 0.05), - borderBottom: "1px solid rgba(224, 224, 224, 1)", paddingLeft: 0, paddingRight: 24 }, @@ -77,7 +82,9 @@ const TableHead = withStyles(styles, { ({ classes, children, + colSpan, disabled, + dragRows, items, selected, toggleAll, @@ -87,13 +94,21 @@ const TableHead = withStyles(styles, { return ( - - {items && items.length > 0 ? ( + {dragRows && (items === undefined || items.length > 0) && ( + + )} + {(items === undefined || items.length > 0) && ( + toggleAll(items, selected)} /> - ) : null} - + + )} {selected ? ( <> - +
{selected && ( diff --git a/src/components/TablePagination/TablePagination.tsx b/src/components/TablePagination/TablePagination.tsx index b5e9553f3..e36d93f21 100644 --- a/src/components/TablePagination/TablePagination.tsx +++ b/src/components/TablePagination/TablePagination.tsx @@ -11,6 +11,9 @@ import TableCell from "@material-ui/core/TableCell"; import Toolbar from "@material-ui/core/Toolbar"; import React from "react"; +import RowNumberSelect from "@saleor/components/RowNumberSelect"; +import { maybe } from "@saleor/misc"; +import { ListSettings } from "../../types"; import TablePaginationActions from "./TablePaginationActions"; const styles = (theme: Theme) => @@ -50,6 +53,7 @@ const styles = (theme: Theme) => toolbar: { height: 56, minHeight: 56, + paddingLeft: 2, paddingRight: 2 } }); @@ -59,11 +63,13 @@ interface TablePaginationProps extends WithStyles { backIconButtonProps?: Partial; colSpan: number; component?: string | typeof TableCell; + settings?: ListSettings; hasNextPage: boolean; hasPreviousPage: boolean; nextIconButtonProps?: Partial; onNextPage(event); onPreviousPage(event); + onUpdateListSettings?(key: keyof ListSettings, value: any): void; } const TablePagination = withStyles(styles, { name: "TablePagination" })( @@ -73,11 +79,13 @@ const TablePagination = withStyles(styles, { name: "TablePagination" })( classes, colSpan: colSpanProp, component: Component, + settings, hasNextPage, hasPreviousPage, nextIconButtonProps, onNextPage, onPreviousPage, + onUpdateListSettings, ...other }: TablePaginationProps) => { let colSpan; @@ -89,7 +97,15 @@ const TablePagination = withStyles(styles, { name: "TablePagination" })( return ( -
+
+ {maybe(() => settings.rowNumber) && ( + + )} +
diff --git a/src/components/TextFieldWithChoice/TextFieldWithChoice.tsx b/src/components/TextFieldWithChoice/TextFieldWithChoice.tsx index cb64f9bc7..2bf8c6408 100644 --- a/src/components/TextFieldWithChoice/TextFieldWithChoice.tsx +++ b/src/components/TextFieldWithChoice/TextFieldWithChoice.tsx @@ -78,7 +78,7 @@ const TextFieldWithChoice = withStyles(styles, { {ChoiceProps.label} - + {ChoiceProps.values ? : null}
; + [ListViews.SALES_LIST]: ListSettings; + [ListViews.SHIPPING_METHODS_LIST]: ListSettings; + [ListViews.STAFF_MEMBERS_LIST]: ListSettings; + [ListViews.VOUCHER_LIST]: ListSettings; +} +export const defaultListSettings: AppListViewSettings = { + [ListViews.CATEGORY_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.COLLECTION_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.CUSTOMER_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.DRAFT_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.NAVIGATION_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.ORDER_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.PAGES_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.PRODUCT_LIST]: { + columns: ["isPublished", "price", "productType"], + rowNumber: PAGINATE_BY + }, + [ListViews.SALES_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.SHIPPING_METHODS_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.STAFF_MEMBERS_LIST]: { + rowNumber: PAGINATE_BY + }, + [ListViews.VOUCHER_LIST]: { + rowNumber: PAGINATE_BY + } +}; diff --git a/src/configuration/index.tsx b/src/configuration/index.tsx index 9eeeb747e..5cc3beab8 100644 --- a/src/configuration/index.tsx +++ b/src/configuration/index.tsx @@ -1,28 +1,36 @@ import React from "react"; +import { attributeListUrl } from "@saleor/attributes/urls"; +import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useUser from "@saleor/hooks/useUser"; -import { WindowTitle } from "../components/WindowTitle"; -import i18n from "../i18n"; -import Navigation from "../icons/Navigation"; -import Pages from "../icons/Pages"; -import ProductTypes from "../icons/ProductTypes"; -import ShippingMethods from "../icons/ShippingMethods"; -import SiteSettings from "../icons/SiteSettings"; -import StaffMembers from "../icons/StaffMembers"; -import Taxes from "../icons/Taxes"; -import { maybe } from "../misc"; -import { menuListUrl } from "../navigation/urls"; -import { pageListUrl } from "../pages/urls"; -import { productTypeListUrl } from "../productTypes/urls"; -import { shippingZonesListUrl } from "../shipping/urls"; -import { siteSettingsUrl } from "../siteSettings/urls"; -import { staffListUrl } from "../staff/urls"; -import { taxSection } from "../taxes/urls"; -import { PermissionEnum } from "../types/globalTypes"; +import i18n from "@saleor/i18n"; +import Navigation from "@saleor/icons/Navigation"; +import Pages from "@saleor/icons/Pages"; +import ProductTypes from "@saleor/icons/ProductTypes"; +import ShippingMethods from "@saleor/icons/ShippingMethods"; +import SiteSettings from "@saleor/icons/SiteSettings"; +import StaffMembers from "@saleor/icons/StaffMembers"; +import Taxes from "@saleor/icons/Taxes"; +import { maybe } from "@saleor/misc"; +import { menuListUrl } from "@saleor/navigation/urls"; +import { pageListUrl } from "@saleor/pages/urls"; +import { productTypeListUrl } from "@saleor/productTypes/urls"; +import { shippingZonesListUrl } from "@saleor/shipping/urls"; +import { siteSettingsUrl } from "@saleor/siteSettings/urls"; +import { staffListUrl } from "@saleor/staff/urls"; +import { taxSection } from "@saleor/taxes/urls"; +import { PermissionEnum } from "@saleor/types/globalTypes"; import ConfigurationPage, { MenuItem } from "./ConfigurationPage"; export const configurationMenu: MenuItem[] = [ + { + description: i18n.t("Determine attributes used to create product types"), + icon: , + permission: PermissionEnum.MANAGE_PRODUCTS, + title: i18n.t("Attributes"), + url: attributeListUrl() + }, { description: i18n.t("Define types of products you sell"), icon: , diff --git a/src/customers/components/CustomerAddressDialog/CustomerAddressDialog.tsx b/src/customers/components/CustomerAddressDialog/CustomerAddressDialog.tsx index dd2d77d80..1925f7d8c 100644 --- a/src/customers/components/CustomerAddressDialog/CustomerAddressDialog.tsx +++ b/src/customers/components/CustomerAddressDialog/CustomerAddressDialog.tsx @@ -12,9 +12,11 @@ import ConfirmButton, { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Form from "@saleor/components/Form"; -import i18n from "../../../i18n"; -import { maybe } from "../../../misc"; -import { UserError } from "../../../types"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import i18n from "@saleor/i18n"; +import { maybe } from "@saleor/misc"; +import { UserError } from "@saleor/types"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import { AddressTypeInput } from "../../types"; import { CustomerAddresses_user_addresses } from "../../types/CustomerAddresses"; @@ -50,14 +52,14 @@ const CustomerAddressDialog = withStyles(styles, {})( onClose, onConfirm }: CustomerAddressDialogProps & WithStyles) => { + const [countryDisplayName, setCountryDisplayName] = useStateFromProps( + maybe(() => address.country.country, "") + ); const initialForm: AddressTypeInput = { city: maybe(() => address.city, ""), cityArea: maybe(() => address.cityArea, ""), companyName: maybe(() => address.companyName, ""), - country: { - label: maybe(() => address.country.country, ""), - value: maybe(() => address.country.code, "") - }, + country: maybe(() => address.country.code, ""), countryArea: maybe(() => address.countryArea, ""), firstName: maybe(() => address.firstName, ""), lastName: maybe(() => address.lastName, ""), @@ -66,6 +68,16 @@ const CustomerAddressDialog = withStyles(styles, {})( streetAddress1: maybe(() => address.streetAddress1, ""), streetAddress2: maybe(() => address.streetAddress2, "") }; + + const countryChoices = maybe( + () => + countries.map(country => ({ + label: country.label, + value: country.code + })), + [] + ); + return (
- {({ change, data, errors, submit }) => ( - <> - - {variant === "create" - ? i18n.t("Add Address") - : i18n.t("Edit Address")} - - - - - - - - {i18n.t("Save Address", { context: "button" })} - - - - - )} + {({ change, data, errors, submit }) => { + const handleCountrySelect = createSingleAutocompleteSelectHandler( + change, + setCountryDisplayName, + countryChoices + ); + + return ( + <> + + {variant === "create" + ? i18n.t("Add Address") + : i18n.t("Edit Address")} + + + + + + + + {i18n.t("Save Address", { context: "button" })} + + + + + ); + }}
); diff --git a/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.tsx b/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.tsx index eb4e13fc7..9ba4fa0c2 100644 --- a/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.tsx +++ b/src/customers/components/CustomerCreateAddress/CustomerCreateAddress.tsx @@ -7,10 +7,10 @@ import React from "react"; import AddressEdit from "@saleor/components/AddressEdit"; import CardTitle from "@saleor/components/CardTitle"; import { FormSpacer } from "@saleor/components/FormSpacer"; +import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; import i18n from "../../../i18n"; import { FormErrors } from "../../../types"; import { AddressTypeInput } from "../../types"; -import { CustomerCreateData_shop_countries } from "../../types/CustomerCreateData"; const styles = createStyles({ overflow: { @@ -19,11 +19,13 @@ const styles = createStyles({ }); export interface CustomerCreateAddressProps extends WithStyles { - countries: CustomerCreateData_shop_countries[]; + countries: SingleAutocompleteChoiceType[]; + countryDisplayName: string; data: AddressTypeInput; disabled: boolean; errors: FormErrors; onChange(event: React.ChangeEvent); + onCountryChange(event: React.ChangeEvent); } const CustomerCreateAddress = withStyles(styles, { @@ -32,10 +34,12 @@ const CustomerCreateAddress = withStyles(styles, { ({ classes, countries, + countryDisplayName, data, disabled, errors, - onChange + onChange, + onCountryChange }: CustomerCreateAddressProps) => ( @@ -45,14 +49,13 @@ const CustomerCreateAddress = withStyles(styles, {
({ - code: country.code, - label: country.country - }))} + countries={countries} data={data} disabled={disabled} + countryDisplayValue={countryDisplayName} errors={errors} onChange={onChange} + onCountryChange={onCountryChange} /> diff --git a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx index 1894148a6..a38cc3cbb 100644 --- a/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx +++ b/src/customers/components/CustomerCreatePage/CustomerCreatePage.tsx @@ -8,6 +8,7 @@ import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import i18n from "../../../i18n"; import { UserError } from "../../../types"; import { AddressTypeInput } from "../../types"; @@ -27,10 +28,7 @@ const initialForm: CustomerCreatePageFormData = { city: "", cityArea: "", companyName: "", - country: { - label: "", - value: "" - }, + country: "", countryArea: "", customerFirstName: "", customerLastName: "", @@ -60,46 +58,69 @@ const CustomerCreatePage: React.StatelessComponent = ({ saveButtonBar, onBack, onSubmit -}: CustomerCreatePageProps) => ( -
- {({ change, data, errors: formErrors, hasChanged, submit }) => ( - - {i18n.t("Customers")} - - -
- { + const [countryDisplayName, setCountryDisplayName] = React.useState(""); + const countryChoices = countries.map(country => ({ + label: country.country, + value: country.code + })); + + return ( + + {({ change, data, errors: formErrors, hasChanged, submit }) => { + const handleCountrySelect = createSingleAutocompleteSelectHandler( + change, + setCountryDisplayName, + countryChoices + ); + + return ( + + {i18n.t("Customers")} + + +
+ + + + + +
+
+ - - - - -
-
- -
- )} -
-); + + ); + }} + + ); +}; CustomerCreatePage.displayName = "CustomerCreatePage"; export default CustomerCreatePage; diff --git a/src/customers/components/CustomerDetails/CustomerDetails.tsx b/src/customers/components/CustomerDetails/CustomerDetails.tsx index 6ec4ffd0f..c564a56fe 100644 --- a/src/customers/components/CustomerDetails/CustomerDetails.tsx +++ b/src/customers/components/CustomerDetails/CustomerDetails.tsx @@ -8,7 +8,7 @@ import { } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; -import * as moment from "moment-timezone"; +import moment from "moment-timezone"; import React from "react"; import CardTitle from "@saleor/components/CardTitle"; diff --git a/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx b/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx index 1e417f960..ca41daeeb 100644 --- a/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx +++ b/src/customers/components/CustomerDetailsPage/CustomerDetailsPage.tsx @@ -55,11 +55,11 @@ const CustomerDetailsPage: React.StatelessComponent<
customer.email), - firstName: maybe(() => customer.firstName), + email: maybe(() => customer.email, ""), + firstName: maybe(() => customer.firstName, ""), isActive: maybe(() => customer.isActive, false), - lastName: maybe(() => customer.lastName), - note: maybe(() => customer.note) + lastName: maybe(() => customer.lastName, ""), + note: maybe(() => customer.note, "") }} onSubmit={onSubmit} confirmLeave diff --git a/src/customers/components/CustomerList/CustomerList.tsx b/src/customers/components/CustomerList/CustomerList.tsx index 8e2b1f17f..5381f23f6 100644 --- a/src/customers/components/CustomerList/CustomerList.tsx +++ b/src/customers/components/CustomerList/CustomerList.tsx @@ -16,9 +16,9 @@ import Checkbox from "@saleor/components/Checkbox"; import Skeleton from "@saleor/components/Skeleton"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; -import { getUserName, maybe, renderCollection } from "../../../misc"; -import { ListActions, ListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { getUserName, maybe, renderCollection } from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; import { ListCustomers_customers_edges_node } from "../../types/ListCustomers"; const styles = (theme: Theme) => @@ -47,14 +47,18 @@ export interface CustomerListProps customers: ListCustomers_customers_edges_node[]; } +const numberOfColumns = 4; + const CustomerList = withStyles(styles, { name: "CustomerList" })( ({ classes, + settings, disabled, customers, pageInfo, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick, toolbar, toggle, @@ -65,6 +69,7 @@ const CustomerList = withStyles(styles, { name: "CustomerList" })( toggle(customer.id)} /> @@ -132,7 +140,7 @@ const CustomerList = withStyles(styles, { name: "CustomerList" })( }, () => ( - + {i18n.t("No customers found")} diff --git a/src/customers/components/CustomerListPage/CustomerListPage.tsx b/src/customers/components/CustomerListPage/CustomerListPage.tsx index 74e5aa6f3..e39dad20c 100644 --- a/src/customers/components/CustomerListPage/CustomerListPage.tsx +++ b/src/customers/components/CustomerListPage/CustomerListPage.tsx @@ -4,8 +4,8 @@ import React from "react"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import i18n from "../../../i18n"; -import { ListActions, PageListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { ListActions, PageListProps } from "@saleor/types"; import { ListCustomers_customers_edges_node } from "../../types/ListCustomers"; import CustomerList from "../CustomerList/CustomerList"; diff --git a/src/customers/types.ts b/src/customers/types.ts index 1eee8acac..fa3a76f4e 100644 --- a/src/customers/types.ts +++ b/src/customers/types.ts @@ -2,10 +2,7 @@ export interface AddressTypeInput { city: string; cityArea?: string; companyName?: string; - country: { - label: string; - value: string; - }; + country: string; countryArea?: string; firstName: string; lastName: string; diff --git a/src/customers/views/CustomerAddresses.tsx b/src/customers/views/CustomerAddresses.tsx index bbb4fb0dc..9f72bce6a 100644 --- a/src/customers/views/CustomerAddresses.tsx +++ b/src/customers/views/CustomerAddresses.tsx @@ -133,6 +133,16 @@ const CustomerAddresses: React.FC = ({ [] ) ); + + const countryChoices = maybe( + () => + shop.countries.map(country => ({ + code: country.code, + label: country.country + })), + [] + ); + return ( <> = ({ - shop.countries.map(country => ({ - code: country.code, - label: country.country - })), - [] - )} + countries={countryChoices} errors={maybe( () => createCustomerAddressOpts.data.addressCreate @@ -173,14 +176,11 @@ const CustomerAddresses: React.FC = ({ open={params.action === "add"} variant="create" onClose={closeModal} - onConfirm={formData => + onConfirm={input => createCustomerAddress({ variables: { id, - input: { - ...formData, - country: formData.country.value - } + input } }) } @@ -192,7 +192,7 @@ const CustomerAddresses: React.FC = ({ ) )} confirmButtonState={updateAddressTransitionState} - countries={[]} + countries={countryChoices} errors={maybe( () => updateCustomerAddressOpts.data.addressUpdate @@ -202,14 +202,11 @@ const CustomerAddresses: React.FC = ({ open={params.action === "edit"} variant="edit" onClose={closeModal} - onConfirm={formData => + onConfirm={input => updateCustomerAddress({ variables: { id: params.id, - input: { - ...formData, - country: formData.country.value - } + input } }) } diff --git a/src/customers/views/CustomerCreate.tsx b/src/customers/views/CustomerCreate.tsx index f89a867d6..10b5719d0 100644 --- a/src/customers/views/CustomerCreate.tsx +++ b/src/customers/views/CustomerCreate.tsx @@ -67,14 +67,8 @@ export const CustomerCreate: React.StatelessComponent<{}> = () => { createCustomer({ variables: { input: { - defaultBillingAddress: { - ...address, - country: address.country.value - }, - defaultShippingAddress: { - ...address, - country: address.country.value - }, + defaultBillingAddress: address, + defaultShippingAddress: address, email: formData.email, firstName: formData.customerFirstName, lastName: formData.customerLastName, diff --git a/src/customers/views/CustomerList.tsx b/src/customers/views/CustomerList.tsx index 443dc1e9c..b36ed9e2a 100644 --- a/src/customers/views/CustomerList.tsx +++ b/src/customers/views/CustomerList.tsx @@ -5,14 +5,15 @@ import React from "react"; import ActionDialog from "@saleor/components/ActionDialog"; import useBulkActions from "@saleor/hooks/useBulkActions"; +import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; -import { PAGINATE_BY } from "../../config"; -import i18n from "../../i18n"; -import { getMutationState, maybe } from "../../misc"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; import CustomerListPage from "../components/CustomerListPage"; import { TypedBulkRemoveCustomers } from "../mutations"; import { TypedCustomerListQuery } from "../queries"; @@ -37,6 +38,9 @@ export const CustomerList: React.StatelessComponent = ({ const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); + const { updateListSettings, settings } = useListSettings( + ListViews.CUSTOMER_LIST + ); const closeModal = () => navigate( @@ -48,7 +52,7 @@ export const CustomerList: React.StatelessComponent = ({ true ); - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); return ( @@ -87,11 +91,13 @@ export const CustomerList: React.StatelessComponent = ({ customers={maybe(() => data.customers.edges.map(edge => edge.node) )} + settings={settings} disabled={loading} pageInfo={pageInfo} onAdd={() => navigate(customerAddUrl)} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(customerUrl(id))} toolbar={ width: "60%" } }); + +const numberOfColumns = 4; + const DiscountCategories = withStyles(styles, { name: "DiscountCategories" })( @@ -71,9 +74,7 @@ const DiscountCategories = withStyles(styles, { }: DiscountCategoriesProps & WithStyles) => ( sale.name) - })} + title={i18n.t("Eligible Categories")} toolbar={
sale.categories.edges.map(edge => edge.node))} @@ -89,7 +91,6 @@ const DiscountCategories = withStyles(styles, { toolbar={toolbar} > <> - {i18n.t("Category name")} @@ -102,7 +103,7 @@ const DiscountCategories = withStyles(styles, { toggle(category.id)} /> @@ -158,7 +160,7 @@ const DiscountCategories = withStyles(styles, { }, () => ( - + {i18n.t("No categories found")} diff --git a/src/discounts/components/DiscountCollections/DiscountCollections.tsx b/src/discounts/components/DiscountCollections/DiscountCollections.tsx index e5b0eff8a..f863204db 100644 --- a/src/discounts/components/DiscountCollections/DiscountCollections.tsx +++ b/src/discounts/components/DiscountCollections/DiscountCollections.tsx @@ -50,6 +50,9 @@ const styles = (theme: Theme) => width: "60%" } }); + +const numberOfColumns = 4; + const DiscountCollections = withStyles(styles, { name: "DiscountCollections" })( @@ -71,9 +74,7 @@ const DiscountCollections = withStyles(styles, { }: DiscountCollectionsProps & WithStyles) => ( sale.name) - })} + title={i18n.t("Eligible Collections")} toolbar={
sale.collections.edges.map(edge => edge.node))} @@ -94,11 +96,12 @@ const DiscountCollections = withStyles(styles, { {i18n.t("Products")} + toggle(collection.id)} /> @@ -156,7 +160,7 @@ const DiscountCollections = withStyles(styles, { }, () => ( - + {i18n.t("No collections found")} diff --git a/src/discounts/components/DiscountProducts/DiscountProducts.tsx b/src/discounts/components/DiscountProducts/DiscountProducts.tsx index 5fe393395..3910cfad9 100644 --- a/src/discounts/components/DiscountProducts/DiscountProducts.tsx +++ b/src/discounts/components/DiscountProducts/DiscountProducts.tsx @@ -19,7 +19,9 @@ import CardTitle from "@saleor/components/CardTitle"; import Checkbox from "@saleor/components/Checkbox"; import Skeleton from "@saleor/components/Skeleton"; import StatusLabel from "@saleor/components/StatusLabel"; -import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import TableCellAvatar, { + AVATAR_MARGIN +} from "@saleor/components/TableCellAvatar"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; import i18n from "../../../i18n"; @@ -36,28 +38,34 @@ export interface SaleProductsProps extends ListProps, ListActions { const styles = (theme: Theme) => createStyles({ - [theme.breakpoints.up("lg")]: { - colName: {}, - colPublished: { - width: 150 - }, - colType: { - width: 200 - } - }, - colName: {}, - colPublished: {}, - colType: {}, - iconCell: { + colActions: { "&:last-child": { paddingRight: 0 }, width: 48 + theme.spacing.unit / 2 }, + colName: { + width: "auto" + }, + colNameLabel: { + marginLeft: AVATAR_MARGIN + }, + colPublished: { + width: 150 + }, + colType: { + width: 200 + }, + table: { + tableLayout: "fixed" + }, tableRow: { cursor: "pointer" } }); + +const numberOfColumns = 5; + const DiscountProducts = withStyles(styles, { name: "DiscountProducts" })( @@ -79,9 +87,7 @@ const DiscountProducts = withStyles(styles, { }: SaleProductsProps & WithStyles) => ( sale.name) - })} + title={i18n.t("Eligible Products")} toolbar={
sale.products.edges.map(edge => edge.node))} toggleAll={toggleAll} toolbar={toolbar} > - - - {i18n.t("Product name")} + + {i18n.t("Product name")} + {i18n.t("Product Type")} @@ -112,7 +119,7 @@ const DiscountProducts = withStyles(styles, { toggle(product.id)} /> product.thumbnail.url)} - /> - + > {maybe(() => product.name, )} - + {maybe( () => product.productType.name, @@ -170,7 +178,7 @@ const DiscountProducts = withStyles(styles, { )} - + { @@ -186,7 +194,9 @@ const DiscountProducts = withStyles(styles, { }, () => ( - {i18n.t("No products found")} + + {i18n.t("No products found")} + ) )} diff --git a/src/discounts/components/SaleList/SaleList.tsx b/src/discounts/components/SaleList/SaleList.tsx index 42169f3d6..54669a3f1 100644 --- a/src/discounts/components/SaleList/SaleList.tsx +++ b/src/discounts/components/SaleList/SaleList.tsx @@ -19,10 +19,10 @@ import Percent from "@saleor/components/Percent"; import Skeleton from "@saleor/components/Skeleton"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; -import { maybe, renderCollection } from "../../../misc"; -import { ListActions, ListProps } from "../../../types"; -import { SaleType } from "../../../types/globalTypes"; +import i18n from "@saleor/i18n"; +import { maybe, renderCollection } from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; +import { SaleType } from "@saleor/types/globalTypes"; import { SaleList_sales_edges_node } from "../../types/SaleList"; export interface SaleListProps extends ListProps, ListActions { @@ -59,15 +59,19 @@ const styles = (theme: Theme) => } }); +const numberOfColumns = 5; + const SaleList = withStyles(styles, { name: "SaleList" })( ({ classes, + settings, defaultCurrency, disabled, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick, pageInfo, sales, @@ -80,6 +84,7 @@ const SaleList = withStyles(styles, {
toggle(sale.id)} /> @@ -184,7 +192,9 @@ const SaleList = withStyles(styles, { }, () => ( - {i18n.t("No sales found")} + + {i18n.t("No sales found")} + ) )} diff --git a/src/discounts/components/SaleListPage/SaleListPage.tsx b/src/discounts/components/SaleListPage/SaleListPage.tsx index dcfdc126d..bf0eca395 100644 --- a/src/discounts/components/SaleListPage/SaleListPage.tsx +++ b/src/discounts/components/SaleListPage/SaleListPage.tsx @@ -4,8 +4,8 @@ import React from "react"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import i18n from "../../../i18n"; -import { ListActions, PageListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { ListActions, PageListProps } from "@saleor/types"; import { SaleList_sales_edges_node } from "../../types/SaleList"; import SaleList from "../SaleList"; diff --git a/src/discounts/components/SalePricing/SalePricing.tsx b/src/discounts/components/SalePricing/SalePricing.tsx index 2aed0a2d9..4f5fb3790 100644 --- a/src/discounts/components/SalePricing/SalePricing.tsx +++ b/src/discounts/components/SalePricing/SalePricing.tsx @@ -96,6 +96,9 @@ const SalePricing = withStyles(styles, { label={i18n.t("Start Date")} value={data.startDate} type="date" + InputLabelProps={{ + shrink: true + }} fullWidth /> diff --git a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx index 9d46dda9a..c04902833 100644 --- a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx +++ b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx @@ -11,22 +11,33 @@ import SaveButtonBar from "@saleor/components/SaveButtonBar"; import i18n from "../../../i18n"; import { UserError } from "../../../types"; import { - VoucherDiscountValueType, - VoucherType + DiscountValueTypeEnum, + VoucherTypeEnum } from "../../../types/globalTypes"; +import { RequirementsPicker } from "../../types"; +import VoucherDates from "../VoucherDates"; import VoucherInfo from "../VoucherInfo"; -import VoucherOptions from "../VoucherOptions"; +import VoucherLimits from "../VoucherLimits"; +import VoucherRequirements from "../VoucherRequirements"; +import VoucherTypes from "../VoucherTypes"; +import VoucherValue from "../VoucherValue"; export interface FormData { + applyOncePerCustomer: boolean; applyOncePerOrder: boolean; code: string; - discountType: VoucherDiscountValueType; + discountType: DiscountValueTypeEnum; endDate: string; - minAmountSpent: number; - name: string; + endTime: string; + hasEndDate: boolean; + hasUsageLimit: boolean; + minAmountSpent: string; + minCheckoutItemsQuantity: string; + requirementsPicker: RequirementsPicker; startDate: string; - type: VoucherType; - usageLimit: number; + startTime: string; + type: VoucherTypeEnum; + usageLimit: string; value: number; } @@ -48,15 +59,21 @@ const VoucherCreatePage: React.StatelessComponent = ({ onSubmit }) => { const initialForm: FormData = { + applyOncePerCustomer: false, applyOncePerOrder: false, code: "", - discountType: VoucherDiscountValueType.FIXED, + discountType: DiscountValueTypeEnum.FIXED, endDate: "", - minAmountSpent: 0, - name: "", + endTime: "", + hasEndDate: false, + hasUsageLimit: false, + minAmountSpent: "0", + minCheckoutItemsQuantity: "0", + requirementsPicker: RequirementsPicker.NONE, startDate: "", - type: VoucherType.VALUE, - usageLimit: 0, + startTime: "", + type: VoucherTypeEnum.ENTIRE_ORDER, + usageLimit: "0", value: 0 }; @@ -72,11 +89,28 @@ const VoucherCreatePage: React.StatelessComponent = ({ data={data} errors={formErrors} disabled={disabled} - variant="create" onChange={change} + variant="create" /> - + {data.discountType.toString() !== "SHIPPING" ? ( + + ) : null} + + = ({ onChange={change} /> + + + = ({ collectionListToolbar, productListToolbar }) => { + let requirementsPickerInitValue; + if (maybe(() => voucher.minAmountSpent.amount) > 0) { + requirementsPickerInitValue = RequirementsPicker.ORDER; + } else if (maybe(() => voucher.minCheckoutItemsQuantity) > 0) { + requirementsPickerInitValue = RequirementsPicker.ITEM; + } else { + requirementsPickerInitValue = RequirementsPicker.NONE; + } + const initialForm: FormData = { + applyOncePerCustomer: maybe(() => voucher.applyOncePerCustomer, false), applyOncePerOrder: maybe(() => voucher.applyOncePerOrder, false), code: maybe(() => voucher.code, ""), discountType: maybe( () => voucher.discountValueType, - VoucherDiscountValueType.FIXED + DiscountValueTypeEnum.FIXED ), - endDate: maybe(() => voucher.endDate, ""), - minAmountSpent: maybe(() => voucher.minAmountSpent.amount, 0), - name: maybe(() => voucher.name, ""), - startDate: maybe(() => voucher.startDate, ""), - type: maybe(() => voucher.type, VoucherType.VALUE), - usageLimit: maybe(() => voucher.usageLimit || 0, 0), + endDate: splitDateTime(maybe(() => voucher.endDate, "")).date, + endTime: splitDateTime(maybe(() => voucher.endDate, "")).time, + hasEndDate: maybe(() => !!voucher.endDate), + hasUsageLimit: maybe(() => !!voucher.usageLimit), + minAmountSpent: maybe(() => voucher.minAmountSpent.amount.toString(), "0"), + minCheckoutItemsQuantity: maybe( + () => voucher.minCheckoutItemsQuantity.toString(), + "0" + ), + requirementsPicker: requirementsPickerInitValue, + startDate: splitDateTime(maybe(() => voucher.startDate, "")).date, + startTime: splitDateTime(maybe(() => voucher.startDate, "")).time, + type: maybe(() => voucher.type, VoucherTypeEnum.ENTIRE_ORDER), + usageLimit: maybe(() => voucher.usageLimit.toString(), "0"), value: maybe(() => voucher.discountValue, 0) }; @@ -137,28 +167,37 @@ const VoucherDetailsPage: React.StatelessComponent = ({ {({ change, data, errors: formErrors, hasChanged, submit }) => ( {i18n.t("Vouchers")} - voucher.name)} /> + voucher.code)} />
- - {data.type === VoucherType.CATEGORY || - data.type === VoucherType.COLLECTION || - data.type === VoucherType.PRODUCT ? ( + {data.discountType.toString() !== "SHIPPING" ? ( + + ) : null} + + {data.type === VoucherTypeEnum.SPECIFIC_PRODUCT && + data.discountType.toString() !== "SHIPPING" ? ( <> = ({ /> )} - ) : data.type === VoucherType.SHIPPING ? ( + ) : null} + + {data.discountType.toString() === "SHIPPING" ? ( voucher.countries)} disabled={disabled} emptyText={i18n.t("Voucher applies to all countries")} title={ <> - {i18n.t("Countries assigned to {{ voucherName }}", { - voucherName: maybe(() => voucher.name) - })} + {i18n.t("Countries")} {i18n.t("Vouchers limited to these countries")} @@ -265,6 +304,30 @@ const VoucherDetailsPage: React.StatelessComponent = ({ onCountryUnassign={onCountryUnassign} /> ) : null} + + + + + +
; + errors: FormErrors<"code">; disabled: boolean; variant: "create" | "update"; - onChange: (event: React.ChangeEvent) => void; + onChange: (event: any) => void; } -const styles = (theme: Theme) => - createStyles({ - nameInput: { - gridColumnEnd: "span 2" - }, - root: { - display: "grid", - gridColumnGap: theme.spacing.unit * 2 + "px", - gridTemplateColumns: "1fr 1fr" - } - }); +const VoucherInfo = ({ + data, + disabled, + errors, + variant, + onChange +}: VoucherInfoProps) => { + const onGenerateCode = () => + onChange({ + target: { + name: "code", + value: generateCode(10) + } + }); -const VoucherInfo = withStyles(styles, { - name: "VoucherInfo" -})( - ({ - classes, - data, - disabled, - errors, - variant, - onChange - }: VoucherInfoProps & WithStyles) => { - const translatedVoucherTypes = translateVoucherTypes(); - const voucherTypeChoices = Object.values(VoucherType).map(type => ({ - label: translatedVoucherTypes[type], - value: type - })); - - return ( - - - - - -
- - -
-
-
- ); - } -); + return ( + + + {i18n.t("Generate Code")} + + ) + } + /> + + + + + ); +}; export default VoucherInfo; diff --git a/src/discounts/components/VoucherList/VoucherList.tsx b/src/discounts/components/VoucherList/VoucherList.tsx index a5e53dfeb..0c8dda994 100644 --- a/src/discounts/components/VoucherList/VoucherList.tsx +++ b/src/discounts/components/VoucherList/VoucherList.tsx @@ -19,10 +19,10 @@ import Percent from "@saleor/components/Percent"; import Skeleton from "@saleor/components/Skeleton"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; -import { maybe, renderCollection } from "../../../misc"; -import { ListActions, ListProps } from "../../../types"; -import { VoucherDiscountValueType } from "../../../types/globalTypes"; +import i18n from "@saleor/i18n"; +import { maybe, renderCollection } from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; +import { DiscountValueTypeEnum } from "@saleor/types/globalTypes"; import { VoucherList_vouchers_edges_node } from "../../types/VoucherList"; export interface VoucherListProps extends ListProps, ListActions { @@ -74,15 +74,19 @@ const styles = (theme: Theme) => } }); +const numberOfColumns = 7; + const VoucherList = withStyles(styles, { name: "VoucherList" })( ({ classes, + settings, defaultCurrency, disabled, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick, pageInfo, vouchers, @@ -95,6 +99,7 @@ const VoucherList = withStyles(styles, {
- {i18n.t("Name", { + {i18n.t("Code", { context: "voucher list table header" })} @@ -135,9 +140,11 @@ const VoucherList = withStyles(styles, { toggle(voucher.id)} /> - {maybe(() => voucher.name, )} + {maybe(() => voucher.code, )} {voucher && voucher.minAmountSpent ? ( @@ -202,7 +210,7 @@ const VoucherList = withStyles(styles, { voucher.discountValueType && voucher.discountValue ? ( voucher.discountValueType === - VoucherDiscountValueType.FIXED ? ( + DiscountValueTypeEnum.FIXED ? ( ( - {i18n.t("No vouchers found")} + + {i18n.t("No vouchers found")} + ) )} diff --git a/src/discounts/components/VoucherListPage/VoucherListPage.tsx b/src/discounts/components/VoucherListPage/VoucherListPage.tsx index 72cc4f4f2..f8b75c59a 100644 --- a/src/discounts/components/VoucherListPage/VoucherListPage.tsx +++ b/src/discounts/components/VoucherListPage/VoucherListPage.tsx @@ -4,8 +4,8 @@ import React from "react"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import i18n from "../../../i18n"; -import { ListActions, PageListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { ListActions, PageListProps } from "@saleor/types"; import { VoucherList_vouchers_edges_node } from "../../types/VoucherList"; import VoucherList from "../VoucherList"; @@ -17,9 +17,11 @@ export interface VoucherListPageProps extends PageListProps, ListActions { const VoucherListPage: React.StatelessComponent = ({ defaultCurrency, disabled, + settings, onAdd, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick, pageInfo, vouchers, @@ -38,9 +40,11 @@ const VoucherListPage: React.StatelessComponent = ({ = ({ - {i18n.t("Name")} + {i18n.t("Code")} - {maybe(() => voucher.name, )} + {maybe(() => voucher.code, )} @@ -51,7 +51,7 @@ const VoucherSummary: React.StatelessComponent = ({ {maybe( () => - voucher.discountValueType === VoucherDiscountValueType.FIXED ? ( + voucher.discountValueType === DiscountValueTypeEnum.FIXED ? ( ({ - [VoucherType.CATEGORY]: i18n.t("Selected Categories"), - [VoucherType.COLLECTION]: i18n.t("Selected Collections"), - [VoucherType.PRODUCT]: i18n.t("Selected Products"), - [VoucherType.SHIPPING]: i18n.t("Shipment"), - [VoucherType.VALUE]: i18n.t("All Products") + [VoucherTypeEnum.SHIPPING]: i18n.t("Shipment"), + [VoucherTypeEnum.ENTIRE_ORDER]: i18n.t("Entire order"), + [VoucherTypeEnum.SPECIFIC_PRODUCT]: i18n.t("Specific Products") }); diff --git a/src/discounts/types/VoucherCataloguesAdd.ts b/src/discounts/types/VoucherCataloguesAdd.ts index f005d2e19..51f997373 100644 --- a/src/discounts/types/VoucherCataloguesAdd.ts +++ b/src/discounts/types/VoucherCataloguesAdd.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { CatalogueInput, VoucherDiscountValueType, VoucherType } from "./../../types/globalTypes"; +import { CatalogueInput, DiscountValueTypeEnum, VoucherTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: VoucherCataloguesAdd @@ -133,18 +133,19 @@ export interface VoucherCataloguesAdd_voucherCataloguesAdd_voucher_categories { export interface VoucherCataloguesAdd_voucherCataloguesAdd_voucher { __typename: "Voucher"; id: string; - name: string | null; + code: string; startDate: any; endDate: any | null; usageLimit: number | null; - discountValueType: VoucherDiscountValueType; + discountValueType: DiscountValueTypeEnum; discountValue: number; countries: (VoucherCataloguesAdd_voucherCataloguesAdd_voucher_countries | null)[] | null; minAmountSpent: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_minAmountSpent | null; - type: VoucherType; - code: string; + minCheckoutItemsQuantity: number | null; + type: VoucherTypeEnum; used: number; applyOncePerOrder: boolean; + applyOncePerCustomer: boolean; products: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_products | null; collections: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_collections | null; categories: VoucherCataloguesAdd_voucherCataloguesAdd_voucher_categories | null; diff --git a/src/discounts/types/VoucherCataloguesRemove.ts b/src/discounts/types/VoucherCataloguesRemove.ts index 417934c69..12cbb13da 100644 --- a/src/discounts/types/VoucherCataloguesRemove.ts +++ b/src/discounts/types/VoucherCataloguesRemove.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { CatalogueInput, VoucherDiscountValueType, VoucherType } from "./../../types/globalTypes"; +import { CatalogueInput, DiscountValueTypeEnum, VoucherTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: VoucherCataloguesRemove @@ -133,18 +133,19 @@ export interface VoucherCataloguesRemove_voucherCataloguesRemove_voucher_categor export interface VoucherCataloguesRemove_voucherCataloguesRemove_voucher { __typename: "Voucher"; id: string; - name: string | null; + code: string; startDate: any; endDate: any | null; usageLimit: number | null; - discountValueType: VoucherDiscountValueType; + discountValueType: DiscountValueTypeEnum; discountValue: number; countries: (VoucherCataloguesRemove_voucherCataloguesRemove_voucher_countries | null)[] | null; minAmountSpent: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_minAmountSpent | null; - type: VoucherType; - code: string; + minCheckoutItemsQuantity: number | null; + type: VoucherTypeEnum; used: number; applyOncePerOrder: boolean; + applyOncePerCustomer: boolean; products: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_products | null; collections: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_collections | null; categories: VoucherCataloguesRemove_voucherCataloguesRemove_voucher_categories | null; diff --git a/src/discounts/types/VoucherCreate.ts b/src/discounts/types/VoucherCreate.ts index eca02664f..e45cb1d5b 100644 --- a/src/discounts/types/VoucherCreate.ts +++ b/src/discounts/types/VoucherCreate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { VoucherInput, VoucherDiscountValueType } from "./../../types/globalTypes"; +import { VoucherInput, DiscountValueTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: VoucherCreate @@ -29,14 +29,15 @@ export interface VoucherCreate_voucherCreate_voucher_minAmountSpent { export interface VoucherCreate_voucherCreate_voucher { __typename: "Voucher"; id: string; - name: string | null; + code: string; startDate: any; endDate: any | null; usageLimit: number | null; - discountValueType: VoucherDiscountValueType; + discountValueType: DiscountValueTypeEnum; discountValue: number; countries: (VoucherCreate_voucherCreate_voucher_countries | null)[] | null; minAmountSpent: VoucherCreate_voucherCreate_voucher_minAmountSpent | null; + minCheckoutItemsQuantity: number | null; } export interface VoucherCreate_voucherCreate { diff --git a/src/discounts/types/VoucherDetails.ts b/src/discounts/types/VoucherDetails.ts index dae5f8b1a..e6efc06b3 100644 --- a/src/discounts/types/VoucherDetails.ts +++ b/src/discounts/types/VoucherDetails.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { VoucherDiscountValueType, VoucherType } from "./../../types/globalTypes"; +import { DiscountValueTypeEnum, VoucherTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL query operation: VoucherDetails @@ -127,18 +127,19 @@ export interface VoucherDetails_voucher_categories { export interface VoucherDetails_voucher { __typename: "Voucher"; id: string; - name: string | null; + code: string; startDate: any; endDate: any | null; usageLimit: number | null; - discountValueType: VoucherDiscountValueType; + discountValueType: DiscountValueTypeEnum; discountValue: number; countries: (VoucherDetails_voucher_countries | null)[] | null; minAmountSpent: VoucherDetails_voucher_minAmountSpent | null; - type: VoucherType; - code: string; + minCheckoutItemsQuantity: number | null; + type: VoucherTypeEnum; used: number; applyOncePerOrder: boolean; + applyOncePerCustomer: boolean; products: VoucherDetails_voucher_products | null; collections: VoucherDetails_voucher_collections | null; categories: VoucherDetails_voucher_categories | null; diff --git a/src/discounts/types/VoucherDetailsFragment.ts b/src/discounts/types/VoucherDetailsFragment.ts index b34b7d793..f8481e68d 100644 --- a/src/discounts/types/VoucherDetailsFragment.ts +++ b/src/discounts/types/VoucherDetailsFragment.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { VoucherDiscountValueType, VoucherType } from "./../../types/globalTypes"; +import { DiscountValueTypeEnum, VoucherTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL fragment: VoucherDetailsFragment @@ -127,18 +127,19 @@ export interface VoucherDetailsFragment_categories { export interface VoucherDetailsFragment { __typename: "Voucher"; id: string; - name: string | null; + code: string; startDate: any; endDate: any | null; usageLimit: number | null; - discountValueType: VoucherDiscountValueType; + discountValueType: DiscountValueTypeEnum; discountValue: number; countries: (VoucherDetailsFragment_countries | null)[] | null; minAmountSpent: VoucherDetailsFragment_minAmountSpent | null; - type: VoucherType; - code: string; + minCheckoutItemsQuantity: number | null; + type: VoucherTypeEnum; used: number; applyOncePerOrder: boolean; + applyOncePerCustomer: boolean; products: VoucherDetailsFragment_products | null; collections: VoucherDetailsFragment_collections | null; categories: VoucherDetailsFragment_categories | null; diff --git a/src/discounts/types/VoucherFragment.ts b/src/discounts/types/VoucherFragment.ts index c8d8aab47..62a8483a4 100644 --- a/src/discounts/types/VoucherFragment.ts +++ b/src/discounts/types/VoucherFragment.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { VoucherDiscountValueType } from "./../../types/globalTypes"; +import { DiscountValueTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL fragment: VoucherFragment @@ -23,12 +23,13 @@ export interface VoucherFragment_minAmountSpent { export interface VoucherFragment { __typename: "Voucher"; id: string; - name: string | null; + code: string; startDate: any; endDate: any | null; usageLimit: number | null; - discountValueType: VoucherDiscountValueType; + discountValueType: DiscountValueTypeEnum; discountValue: number; countries: (VoucherFragment_countries | null)[] | null; minAmountSpent: VoucherFragment_minAmountSpent | null; + minCheckoutItemsQuantity: number | null; } diff --git a/src/discounts/types/VoucherList.ts b/src/discounts/types/VoucherList.ts index cce0df721..9b421ef41 100644 --- a/src/discounts/types/VoucherList.ts +++ b/src/discounts/types/VoucherList.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { VoucherDiscountValueType } from "./../../types/globalTypes"; +import { DiscountValueTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL query operation: VoucherList @@ -23,14 +23,15 @@ export interface VoucherList_vouchers_edges_node_minAmountSpent { export interface VoucherList_vouchers_edges_node { __typename: "Voucher"; id: string; - name: string | null; + code: string; startDate: any; endDate: any | null; usageLimit: number | null; - discountValueType: VoucherDiscountValueType; + discountValueType: DiscountValueTypeEnum; discountValue: number; countries: (VoucherList_vouchers_edges_node_countries | null)[] | null; minAmountSpent: VoucherList_vouchers_edges_node_minAmountSpent | null; + minCheckoutItemsQuantity: number | null; } export interface VoucherList_vouchers_edges { diff --git a/src/discounts/types/VoucherUpdate.ts b/src/discounts/types/VoucherUpdate.ts index 2dd2521b2..1dd2e53c6 100644 --- a/src/discounts/types/VoucherUpdate.ts +++ b/src/discounts/types/VoucherUpdate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { VoucherInput, VoucherDiscountValueType } from "./../../types/globalTypes"; +import { VoucherInput, DiscountValueTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: VoucherUpdate @@ -29,14 +29,15 @@ export interface VoucherUpdate_voucherUpdate_voucher_minAmountSpent { export interface VoucherUpdate_voucherUpdate_voucher { __typename: "Voucher"; id: string; - name: string | null; + code: string; startDate: any; endDate: any | null; usageLimit: number | null; - discountValueType: VoucherDiscountValueType; + discountValueType: DiscountValueTypeEnum; discountValue: number; countries: (VoucherUpdate_voucherUpdate_voucher_countries | null)[] | null; minAmountSpent: VoucherUpdate_voucherUpdate_voucher_minAmountSpent | null; + minCheckoutItemsQuantity: number | null; } export interface VoucherUpdate_voucherUpdate { diff --git a/src/discounts/views/SaleDetails.tsx b/src/discounts/views/SaleDetails.tsx index cc50292ed..d74c74531 100644 --- a/src/discounts/views/SaleDetails.tsx +++ b/src/discounts/views/SaleDetails.tsx @@ -339,13 +339,13 @@ export const SaleDetails: React.StatelessComponent = ({ onFetch={searchProducts} loading={searchProductsOpts.loading} onClose={closeModal} - onSubmit={formData => + onSubmit={products => saleCataloguesAdd({ variables: { ...paginationState, id, input: { - products: formData.products.map( + products: products.map( product => product.id ) } @@ -383,13 +383,13 @@ export const SaleDetails: React.StatelessComponent = ({ onFetch={searchCategories} loading={searchCategoriesOpts.loading} onClose={closeModal} - onSubmit={formData => + onSubmit={categories => saleCataloguesAdd({ variables: { ...paginationState, id, input: { - categories: formData.categories.map( + categories: categories.map( product => product.id ) } @@ -420,13 +420,13 @@ export const SaleDetails: React.StatelessComponent = ({ onFetch={searchCollections} loading={searchCollectionsOpts.loading} onClose={closeModal} - onSubmit={formData => + onSubmit={collections => saleCataloguesAdd({ variables: { ...paginationState, id, input: { - collections: formData.collections.map( + collections: collections.map( product => product.id ) } diff --git a/src/discounts/views/SaleList.tsx b/src/discounts/views/SaleList.tsx index b51ba16f9..c159bf502 100644 --- a/src/discounts/views/SaleList.tsx +++ b/src/discounts/views/SaleList.tsx @@ -6,15 +6,16 @@ import React from "react"; import ActionDialog from "@saleor/components/ActionDialog"; import { WindowTitle } from "@saleor/components/WindowTitle"; import useBulkActions from "@saleor/hooks/useBulkActions"; +import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; import useShop from "@saleor/hooks/useShop"; -import { PAGINATE_BY } from "../../config"; -import i18n from "../../i18n"; -import { getMutationState, maybe } from "../../misc"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; import SaleListPage from "../components/SaleListPage"; import { TypedSaleBulkDelete } from "../mutations"; import { TypedSaleList } from "../queries"; @@ -40,10 +41,13 @@ export const SaleList: React.StatelessComponent = ({ const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); + const { updateListSettings, settings } = useListSettings( + ListViews.SALES_LIST + ); const closeModal = () => navigate(saleListUrl(), true); - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); return ( @@ -86,11 +90,13 @@ export const SaleList: React.StatelessComponent = ({ shop.defaultCurrency)} sales={maybe(() => data.sales.edges.map(edge => edge.node))} + settings={settings} disabled={loading} pageInfo={pageInfo} onAdd={() => navigate(saleAddUrl)} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(saleUrl(id))} isChecked={isSelected} selected={listElements.length} diff --git a/src/discounts/views/VoucherCreate.tsx b/src/discounts/views/VoucherCreate.tsx index 331a6a6db..cc4a7bc33 100644 --- a/src/discounts/views/VoucherCreate.tsx +++ b/src/discounts/views/VoucherCreate.tsx @@ -5,25 +5,17 @@ import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import useShop from "@saleor/hooks/useShop"; import i18n from "../../i18n"; -import { decimal, getMutationState, maybe } from "../../misc"; +import { decimal, getMutationState, joinDateTime, maybe } from "../../misc"; import { DiscountValueTypeEnum, - VoucherDiscountValueType, VoucherTypeEnum } from "../../types/globalTypes"; import VoucherCreatePage from "../components/VoucherCreatePage"; import { TypedVoucherCreate } from "../mutations"; +import { RequirementsPicker } from "../types"; import { VoucherCreate } from "../types/VoucherCreate"; import { voucherListUrl, voucherUrl } from "../urls"; -function discountValueTypeEnum( - type: VoucherDiscountValueType -): DiscountValueTypeEnum { - return type.toString() === DiscountValueTypeEnum.FIXED - ? DiscountValueTypeEnum.FIXED - : DiscountValueTypeEnum.PERCENTAGE; -} - export const VoucherDetails: React.StatelessComponent = () => { const navigate = useNavigator(); const notify = useNotifier(); @@ -61,18 +53,37 @@ export const VoucherDetails: React.StatelessComponent = () => { voucherCreate({ variables: { input: { + applyOncePerCustomer: formData.applyOncePerCustomer, + applyOncePerOrder: formData.applyOncePerOrder, code: formData.code, - discountValue: decimal(formData.value), - discountValueType: discountValueTypeEnum( - formData.discountType + discountValue: + formData.discountType.toString() === "SHIPPING" + ? 100 + : decimal(formData.value), + discountValueType: + formData.discountType.toString() === "SHIPPING" + ? DiscountValueTypeEnum.PERCENTAGE + : formData.discountType, + endDate: formData.hasEndDate + ? joinDateTime(formData.endDate, formData.endTime) + : null, + minAmountSpent: + formData.requirementsPicker !== RequirementsPicker.ORDER + ? 0 + : parseFloat(formData.minAmountSpent), + minCheckoutItemsQuantity: + formData.requirementsPicker !== RequirementsPicker.ITEM + ? 0 + : parseFloat(formData.minCheckoutItemsQuantity), + startDate: joinDateTime( + formData.startDate, + formData.startTime ), - endDate: - formData.endDate === "" ? null : formData.endDate, - minAmountSpent: formData.minAmountSpent, - name: formData.name, - startDate: - formData.startDate === "" ? null : formData.startDate, - type: VoucherTypeEnum[formData.type] + type: + formData.discountType.toString() === "SHIPPING" + ? VoucherTypeEnum.ENTIRE_ORDER + : formData.type, + usageLimit: parseInt(formData.usageLimit, 10) } } }) diff --git a/src/discounts/views/VoucherDetails.tsx b/src/discounts/views/VoucherDetails.tsx index ff8012c98..c7f90799e 100644 --- a/src/discounts/views/VoucherDetails.tsx +++ b/src/discounts/views/VoucherDetails.tsx @@ -21,11 +21,11 @@ import SearchCategories from "../../containers/SearchCategories"; import SearchCollections from "../../containers/SearchCollections"; import SearchProducts from "../../containers/SearchProducts"; import i18n from "../../i18n"; -import { decimal, getMutationState, maybe } from "../../misc"; +import { decimal, getMutationState, joinDateTime, maybe } from "../../misc"; import { productUrl } from "../../products/urls"; import { DiscountValueTypeEnum, - VoucherDiscountValueType + VoucherTypeEnum } from "../../types/globalTypes"; import DiscountCountrySelectDialog from "../components/DiscountCountrySelectDialog"; import VoucherDetailsPage, { @@ -38,6 +38,7 @@ import { TypedVoucherUpdate } from "../mutations"; import { TypedVoucherDetails } from "../queries"; +import { RequirementsPicker } from "../types"; import { VoucherCataloguesAdd } from "../types/VoucherCataloguesAdd"; import { VoucherCataloguesRemove } from "../types/VoucherCataloguesRemove"; import { VoucherDelete } from "../types/VoucherDelete"; @@ -54,14 +55,6 @@ interface VoucherDetailsProps { params: VoucherUrlQueryParams; } -function discountValueTypeEnum( - type: VoucherDiscountValueType -): DiscountValueTypeEnum { - return type.toString() === DiscountValueTypeEnum.FIXED - ? DiscountValueTypeEnum.FIXED - : DiscountValueTypeEnum.PERCENTAGE; -} - export const VoucherDetails: React.StatelessComponent = ({ id, params @@ -323,19 +316,51 @@ export const VoucherDetails: React.StatelessComponent = ({ variables: { id, input: { - discountValue: decimal(formData.value), - discountValueType: discountValueTypeEnum( - formData.discountType + applyOncePerCustomer: + formData.applyOncePerCustomer, + applyOncePerOrder: + formData.applyOncePerOrder, + discountValue: + formData.discountType.toString() === + "SHIPPING" + ? 100 + : decimal(formData.value), + discountValueType: + formData.discountType.toString() === + "SHIPPING" + ? DiscountValueTypeEnum.PERCENTAGE + : formData.discountType, + endDate: formData.hasEndDate + ? joinDateTime( + formData.endDate, + formData.endTime + ) + : null, + minAmountSpent: + formData.requirementsPicker !== + RequirementsPicker.ORDER + ? 0 + : parseFloat(formData.minAmountSpent), + minCheckoutItemsQuantity: + formData.requirementsPicker !== + RequirementsPicker.ITEM + ? 0 + : parseFloat( + formData.minCheckoutItemsQuantity + ), + startDate: joinDateTime( + formData.startDate, + formData.startTime ), - endDate: - formData.endDate === "" - ? null - : formData.endDate, - name: formData.name, - startDate: - formData.startDate === "" - ? null - : formData.startDate + type: + formData.discountType.toString() === + "SHIPPING" + ? VoucherTypeEnum.SHIPPING + : formData.type, + usageLimit: parseInt( + formData.usageLimit, + 10 + ) } } }) @@ -401,13 +426,13 @@ export const VoucherDetails: React.StatelessComponent = ({ onFetch={searchCategories} loading={searchCategoriesOpts.loading} onClose={closeModal} - onSubmit={formData => + onSubmit={categories => voucherCataloguesAdd({ variables: { ...paginationState, id, input: { - categories: formData.categories.map( + categories: categories.map( product => product.id ) } @@ -438,13 +463,13 @@ export const VoucherDetails: React.StatelessComponent = ({ onFetch={searchCollections} loading={searchCollectionsOpts.loading} onClose={closeModal} - onSubmit={formData => + onSubmit={collections => voucherCataloguesAdd({ variables: { ...paginationState, id, input: { - collections: formData.collections.map( + collections: collections.map( product => product.id ) } @@ -490,13 +515,13 @@ export const VoucherDetails: React.StatelessComponent = ({ onFetch={searchProducts} loading={searchProductsOpts.loading} onClose={closeModal} - onSubmit={formData => + onSubmit={products => voucherCataloguesAdd({ variables: { ...paginationState, id, input: { - products: formData.products.map( + products: products.map( product => product.id ) } @@ -597,10 +622,10 @@ export const VoucherDetails: React.StatelessComponent = ({ {{ voucherName }}?", + "Are you sure you want to remove {{ voucherCode }}?", { - voucherName: maybe( - () => data.voucher.name, + voucherCode: maybe( + () => data.voucher.code, "..." ) } diff --git a/src/discounts/views/VoucherList.tsx b/src/discounts/views/VoucherList.tsx index 8d5949a30..fe47592a2 100644 --- a/src/discounts/views/VoucherList.tsx +++ b/src/discounts/views/VoucherList.tsx @@ -6,15 +6,16 @@ import React from "react"; import ActionDialog from "@saleor/components/ActionDialog"; import { WindowTitle } from "@saleor/components/WindowTitle"; import useBulkActions from "@saleor/hooks/useBulkActions"; +import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; import useShop from "@saleor/hooks/useShop"; -import { PAGINATE_BY } from "../../config"; -import i18n from "../../i18n"; -import { getMutationState, maybe } from "../../misc"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; import VoucherListPage from "../components/VoucherListPage"; import { TypedVoucherBulkDelete } from "../mutations"; import { TypedVoucherList } from "../queries"; @@ -40,10 +41,13 @@ export const VoucherList: React.StatelessComponent = ({ const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); + const { updateListSettings, settings } = useListSettings( + ListViews.VOUCHER_LIST + ); const closeModal = () => navigate(voucherListUrl(), true); - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); return ( @@ -84,6 +88,7 @@ export const VoucherList: React.StatelessComponent = ({ shop.defaultCurrency)} + settings={settings} vouchers={maybe(() => data.vouchers.edges.map(edge => edge.node) )} @@ -92,6 +97,7 @@ export const VoucherList: React.StatelessComponent = ({ onAdd={() => navigate(voucherAddUrl)} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(voucherUrl(id))} isChecked={isSelected} selected={listElements.length} diff --git a/src/fixtures.ts b/src/fixtures.ts index ef1d1aeec..10f3345cd 100644 --- a/src/fixtures.ts +++ b/src/fixtures.ts @@ -1,5 +1,10 @@ import { Filter } from "./components/TableFilter"; -import { FilterPageProps, ListActions, PageListProps } from "./types"; +import { + FetchMoreProps, + FilterPageProps, + ListActions, + PageListProps +} from "./types"; const pageInfo = { hasNextPage: true, @@ -12,7 +17,8 @@ export const pageListProps: { [key: string]: PageListProps } = { onNextPage: undefined, onPreviousPage: undefined, onRowClick: () => undefined, - pageInfo + pageInfo, + settings: { rowNumber: 20 } }, loading: { disabled: true, @@ -20,7 +26,8 @@ export const pageListProps: { [key: string]: PageListProps } = { onNextPage: undefined, onPreviousPage: undefined, onRowClick: () => undefined, - pageInfo + pageInfo, + settings: undefined } }; export const listActionsProps: ListActions = { @@ -123,3 +130,10 @@ export const filters: Filter[] = [ ...filter, label: filter.label + filterIndex })); + +export const fetchMoreProps: FetchMoreProps = { + hasMore: true, + loading: false, + onFetch: () => undefined, + onFetchMore: () => undefined +}; diff --git a/src/home/components/HomeHeader/HomeHeader.tsx b/src/home/components/HomeHeader/HomeHeader.tsx index 30c746f1b..a8076669c 100644 --- a/src/home/components/HomeHeader/HomeHeader.tsx +++ b/src/home/components/HomeHeader/HomeHeader.tsx @@ -28,26 +28,24 @@ interface HomeOrdersCardProps extends WithStyles { } const HomeOrdersCard = withStyles(styles, { name: "HomeOrdersCard" })( - ({ classes, userName }: HomeOrdersCardProps) => { - return ( -
- - {userName ? ( - i18n.t("Hello there, {{userName}}", { userName }) - ) : ( - - )} - - - {userName ? ( - i18n.t("Here is some information we gathered about your store") - ) : ( - - )} - -
- ); - } + ({ classes, userName }: HomeOrdersCardProps) => ( +
+ + {userName ? ( + i18n.t("Hello there, {{userName}}", { userName }) + ) : ( + + )} + + + {userName ? ( + i18n.t("Here is some information we gathered about your store") + ) : ( + + )} + +
+ ) ); HomeOrdersCard.displayName = "HomeOrdersCard"; export default HomeOrdersCard; diff --git a/src/home/components/HomeProductListCard/HomeProductListCard.tsx b/src/home/components/HomeProductListCard/HomeProductListCard.tsx index 550ef52a7..a7aaa297b 100644 --- a/src/home/components/HomeProductListCard/HomeProductListCard.tsx +++ b/src/home/components/HomeProductListCard/HomeProductListCard.tsx @@ -10,7 +10,7 @@ import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import TableRow from "@material-ui/core/TableRow"; import Typography from "@material-ui/core/Typography"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import CardTitle from "@saleor/components/CardTitle"; @@ -81,11 +81,7 @@ export const HomeProductList = withStyles(styles, { name: "HomeProductList" })( {maybe(() => variant.attributes - .map(attribute => attribute.value) - .sort((a, b) => - a.sortOrder > b.sortOrder ? 1 : -1 - ) - .map(attribute => attribute.name) + .map(attribute => attribute.value.name) .join(" / ") )} diff --git a/src/home/queries.ts b/src/home/queries.ts index 8e5011fdf..46aee8590 100644 --- a/src/home/queries.ts +++ b/src/home/queries.ts @@ -37,7 +37,6 @@ const home = gql` value { id name - sortOrder } } product { diff --git a/src/home/types/Home.ts b/src/home/types/Home.ts index 9400d0adc..b79c2e0c6 100644 --- a/src/home/types/Home.ts +++ b/src/home/types/Home.ts @@ -54,12 +54,11 @@ export interface Home_productTopToday_edges_node_attributes_value { __typename: "AttributeValue"; id: string; name: string | null; - sortOrder: number | null; } export interface Home_productTopToday_edges_node_attributes { __typename: "SelectedAttribute"; - value: Home_productTopToday_edges_node_attributes_value; + value: Home_productTopToday_edges_node_attributes_value | null; } export interface Home_productTopToday_edges_node_product_thumbnail { diff --git a/src/hooks/useBulkActions.ts b/src/hooks/useBulkActions.ts index 986308782..b4c86fe61 100644 --- a/src/hooks/useBulkActions.ts +++ b/src/hooks/useBulkActions.ts @@ -1,34 +1,22 @@ -import { useState } from "react"; import { Node } from "../types"; +import useListActions from "./useListActions"; function useBulkActions(initial: string[] = []) { - const [listElements, setListElements] = useState(initial); - - function isSelected(id: string) { - return !!listElements.find(listElement => listElement === id); - } - - function add(id: string) { - setListElements([...listElements, id]); - } - - function remove(id: string) { - setListElements(listElements.filter(listElement => listElement !== id)); - } - - function reset() { - setListElements([]); - } - - function toggle(id: string) { - isSelected(id) ? remove(id) : add(id); - } + const { + add, + isSelected, + listElements, + remove, + reset, + set, + toggle + } = useListActions(initial); function toggleAll(items: Node[], selected: number) { const allItems = items.map(item => item.id); reset(); if (selected !== allItems.length) { - setListElements(allItems); + set(allItems); } } diff --git a/src/icons/Calendar.tsx b/src/icons/Calendar.tsx index c7250b42d..3df4eb749 100644 --- a/src/icons/Calendar.tsx +++ b/src/icons/Calendar.tsx @@ -18,7 +18,7 @@ export const Calendar = createSvgIcon( y2="25.7327" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/Draggable.tsx b/src/icons/Draggable.tsx index cdaad7d5e..245d84a6f 100644 --- a/src/icons/Draggable.tsx +++ b/src/icons/Draggable.tsx @@ -21,7 +21,7 @@ export const Draggable = createSvgIcon( y2="10.4478" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/Image.tsx b/src/icons/Image.tsx index 458cbc3c9..00e4e9238 100644 --- a/src/icons/Image.tsx +++ b/src/icons/Image.tsx @@ -16,7 +16,7 @@ export const Image = createSvgIcon( y2="23.5714" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/Navigation.tsx b/src/icons/Navigation.tsx index c7c7d29a9..5e48f814d 100644 --- a/src/icons/Navigation.tsx +++ b/src/icons/Navigation.tsx @@ -24,7 +24,7 @@ export const Navigation = createSvgIcon( y2="46.8339" gradientUnits="userSpaceOnUse" > - + - + diff --git a/src/icons/Orders.tsx b/src/icons/Orders.tsx index 7026b704a..14e6a0246 100644 --- a/src/icons/Orders.tsx +++ b/src/icons/Orders.tsx @@ -18,7 +18,7 @@ export const Orders = createSvgIcon( y2="40.6161" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/Pages.tsx b/src/icons/Pages.tsx index 6aa43bc32..8a74b6472 100644 --- a/src/icons/Pages.tsx +++ b/src/icons/Pages.tsx @@ -18,7 +18,7 @@ export const Pages = createSvgIcon( y2="46.7403" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/ProductTypes.tsx b/src/icons/ProductTypes.tsx index c86899204..a35417ac8 100644 --- a/src/icons/ProductTypes.tsx +++ b/src/icons/ProductTypes.tsx @@ -18,7 +18,7 @@ export const ProductTypes = createSvgIcon( y2="46.78" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/Sales.tsx b/src/icons/Sales.tsx index dae31094f..4aba8d63c 100644 --- a/src/icons/Sales.tsx +++ b/src/icons/Sales.tsx @@ -18,7 +18,7 @@ export const Sales = createSvgIcon( y2="46.3533" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/ShippingMethods.tsx b/src/icons/ShippingMethods.tsx index 0c4c90d03..145bd25dd 100644 --- a/src/icons/ShippingMethods.tsx +++ b/src/icons/ShippingMethods.tsx @@ -18,7 +18,7 @@ export const ShippingMethods = createSvgIcon( y2="47.1429" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/SiteSettings.tsx b/src/icons/SiteSettings.tsx index 38829145d..a0fdac5f8 100644 --- a/src/icons/SiteSettings.tsx +++ b/src/icons/SiteSettings.tsx @@ -18,7 +18,7 @@ export const SiteSettings = createSvgIcon( y2="47.2508" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/StaffMembers.tsx b/src/icons/StaffMembers.tsx index bf1c45162..f583d01a3 100644 --- a/src/icons/StaffMembers.tsx +++ b/src/icons/StaffMembers.tsx @@ -18,7 +18,7 @@ export const StaffMembers = createSvgIcon( y2="46.2508" gradientUnits="userSpaceOnUse" > - + diff --git a/src/icons/Taxes.tsx b/src/icons/Taxes.tsx index ae62d4206..0f910dced 100644 --- a/src/icons/Taxes.tsx +++ b/src/icons/Taxes.tsx @@ -18,7 +18,7 @@ export const Taxes = createSvgIcon( y2="43.2798" gradientUnits="userSpaceOnUse" > - + diff --git a/src/index.tsx b/src/index.tsx index 61c8d3654..63110ff2e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,6 +10,8 @@ import { ApolloProvider } from "react-apollo"; import { render } from "react-dom"; import { BrowserRouter, Route, Switch } from "react-router-dom"; +import AttributeSection from "./attributes"; +import { attributeSection } from "./attributes/urls"; import { getAuthToken, removeAuthToken } from "./auth"; import AuthProvider from "./auth/AuthProvider"; import LoginLoading from "./auth/components/LoginLoading/LoginLoading"; @@ -19,9 +21,7 @@ import Login from "./auth/views/Login"; import CategorySection from "./categories"; import CollectionSection from "./collections"; import { AppProgressProvider } from "./components/AppProgress"; -// import { ConfirmFormLeaveDialog } from "./components/ConfirmFormLeaveDialog"; import { DateProvider } from "./components/Date"; -import { FormProvider } from "./components/Form"; import { LocaleProvider } from "./components/Locale"; import { MessageManager } from "./components/messages"; import { ShopProvider } from "./components/Shop"; @@ -71,12 +71,18 @@ const authLink = setContext((_, context) => { }; }); +// DON'T TOUCH THIS +// These are separate clients and do not share configs between themselves +// so we need to explicitly set them const linkOptions = { credentials: "same-origin", uri: API_URI }; const uploadLink = createUploadLink(linkOptions); -const batchLink = new BatchHttpLink(linkOptions); +const batchLink = new BatchHttpLink({ + batchInterval: 100, + ...linkOptions +}); const link = ApolloLink.split( operation => operation.getContext().useBatching, @@ -102,134 +108,129 @@ const App: React.FC = () => { const isDark = localStorage.getItem("theme") === "true"; return ( - - - - - - - - - - - {/* FIXME: #3424 */} - {/* */} - - {({ - hasToken, - isAuthenticated, - tokenAuthLoading, - tokenVerifyLoading, - user - }) => { - return isAuthenticated && - !tokenAuthLoading && - !tokenVerifyLoading ? ( - + + + + + + + + + + + {({ + hasToken, + isAuthenticated, + tokenAuthLoading, + tokenVerifyLoading, + user + }) => { + return isAuthenticated && + !tokenAuthLoading && + !tokenVerifyLoading ? ( + + + + + + + + + + + + + + + + + + {configurationMenu.filter(menuItem => + hasPermission(menuItem.permission, user) + ).length > 0 && ( - - - - - - - - - - - - - - - {configurationMenu.filter(menuItem => - hasPermission(menuItem.permission, user) - ).length > 0 && ( - - )} - - - ) : hasToken && tokenVerifyLoading ? ( - - ) : ( - - ); - }} - - - - - - - - - - + )} + + + ) : hasToken && tokenVerifyLoading ? ( + + ) : ( + + ); + }} + + + + + + + + + ); }; diff --git a/src/misc.ts b/src/misc.ts index 83f9e9aec..d27eeded2 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -1,6 +1,7 @@ +import moment from "moment-timezone"; +import { MutationFn, MutationResult } from "react-apollo"; import urlJoin from "url-join"; -import { MutationFn, MutationResult } from "react-apollo"; import { ConfirmButtonTransitionState } from "./components/ConfirmButton/ConfirmButton"; import { APP_MOUNT_URI } from "./config"; import { AddressType } from "./customers/types"; @@ -94,10 +95,7 @@ export const transformAddressToForm = (data: AddressType) => ({ city: maybe(() => data.city, ""), cityArea: maybe(() => data.cityArea, ""), companyName: maybe(() => data.companyName, ""), - country: { - label: maybe(() => data.country.country, ""), - value: maybe(() => data.country.code, "") - }, + country: maybe(() => data.country.code, ""), countryArea: maybe(() => data.countryArea, ""), firstName: maybe(() => data.firstName, ""), lastName: maybe(() => data.lastName, ""), @@ -145,7 +143,9 @@ export const translatedAuthorizationKeyTypes = () => ({ [AuthorizationKeyType.GOOGLE_OAUTH2]: i18n.t("Google OAuth2") }); -export function maybe(exp: () => T, d?: T) { +export function maybe(exp: () => T): T | undefined; +export function maybe(exp: () => T, d: T): T; +export function maybe(exp: any, d?: any) { try { const result = exp(); return result === undefined ? d : result; @@ -236,3 +236,38 @@ export function stopPropagation(cb: () => void) { cb(); }; } + +export function joinDateTime(date: string, time?: string) { + if (!date) { + return null; + } + const setTime = time || "00:00"; + const dateTime = moment(date + " " + setTime).format(); + return dateTime; +} + +export function splitDateTime(dateTime: string) { + if (!dateTime) { + return { + date: "", + time: "" + }; + } + // Default html input format YYYY-MM-DD HH:mm + const splitDateTime = moment(dateTime) + .format("YYYY-MM-DD HH:mm") + .split(" "); + return { + date: splitDateTime[0], + time: splitDateTime[1] + }; +} + +export function generateCode(charNum: number) { + let result = ""; + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + for (let i = 0; i < charNum; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} diff --git a/src/navigation/components/MenuItemDialog/MenuItemDialog.tsx b/src/navigation/components/MenuItemDialog/MenuItemDialog.tsx index c607ab133..87c9a0c56 100644 --- a/src/navigation/components/MenuItemDialog/MenuItemDialog.tsx +++ b/src/navigation/components/MenuItemDialog/MenuItemDialog.tsx @@ -4,20 +4,24 @@ import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; import TextField from "@material-ui/core/TextField"; -import * as isUrl from "is-url"; +import Typography from "@material-ui/core/Typography"; +import isUrl from "is-url"; import React from "react"; import AutocompleteSelectMenu from "@saleor/components/AutocompleteSelectMenu"; import ConfirmButton, { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import Form from "@saleor/components/Form"; import FormSpacer from "@saleor/components/FormSpacer"; -import { SearchCategories_categories_edges_node } from "../../../containers/SearchCategories/types/SearchCategories"; -import { SearchCollections_collections_edges_node } from "../../../containers/SearchCollections/types/SearchCollections"; -import { SearchPages_pages_edges_node } from "../../../containers/SearchPages/types/SearchPages"; -import i18n from "../../../i18n"; -import { getMenuItemByValue, IMenu } from "../../../utils/menu"; +import { SearchCategories_categories_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories"; +import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections"; +import { SearchPages_pages_edges_node } from "@saleor/containers/SearchPages/types/SearchPages"; +import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import i18n from "@saleor/i18n"; +import { UserError } from "@saleor/types"; +import { getErrors, getFieldError } from "@saleor/utils/errors"; +import { getMenuItemByValue, IMenu } from "@saleor/utils/menu"; export type MenuItemType = "category" | "collection" | "link" | "page"; export interface MenuItemData { @@ -32,6 +36,7 @@ export interface MenuItemDialogFormData extends MenuItemData { export interface MenuItemDialogProps { confirmButtonState: ConfirmButtonTransitionState; disabled: boolean; + errors: UserError[]; initial?: MenuItemDialogFormData; initialDisplayValue?: string; loading: boolean; @@ -69,6 +74,7 @@ function getDisplayValue(menu: IMenu, value: string): string { const MenuItemDialog: React.StatelessComponent = ({ confirmButtonState, disabled, + errors: apiErrors, initial, initialDisplayValue, loading, @@ -80,9 +86,13 @@ const MenuItemDialog: React.StatelessComponent = ({ collections, pages }) => { + const errors = useModalDialogErrors(apiErrors, open); const [displayValue, setDisplayValue] = React.useState( initialDisplayValue || "" ); + const [data, setData] = useStateFromProps( + initial || defaultInitial + ); const [url, setUrl] = React.useState(undefined); // Refresh initial display value if changed @@ -96,6 +106,8 @@ const MenuItemDialog: React.StatelessComponent = ({ setUrl(undefined); }, [open]); + const mutationErrors = getErrors(errors); + let options: IMenu = []; if (categories.length > 0) { @@ -177,6 +189,23 @@ const MenuItemDialog: React.StatelessComponent = ({ onQueryChange(query); }; + const handleSelectChange = (event: React.ChangeEvent) => { + const value = event.target.value; + const menuItemData = getMenuItemData(value); + + setData(value => ({ + ...value, + ...menuItemData + })); + setDisplayValue(getDisplayValue(options, value)); + }; + + const handleSubmit = () => onSubmit(data); + + const idError = ["category", "collection", "page", "url"] + .map(field => getFieldError(errors, field)) + .reduce((acc, err) => acc || err); + return ( = ({ }} > - {i18n.t("Add Item", { - context: "create new menu item" - })} + {!!initial + ? i18n.t("Edit Item", { + context: "edit menu item" + }) + : i18n.t("Add Item", { + context: "create new menu item" + })} - - {({ change, data, submit }) => { - const handleSelectChange = (event: React.ChangeEvent) => { - const value = event.target.value; - const menuItemData = getMenuItemData(value); - change( - { - target: { - name: "id", - value: menuItemData.id - } - } as any, - () => - change( - { - target: { - name: "type", - value: menuItemData.type - } - } as any, - () => setDisplayValue(getDisplayValue(options, value)) - ) - ); - }; - - return ( - <> - - - - - - - - - {i18n.t("Submit", { context: "button" })} - - - - ); - }} - + + + setData(value => ({ + ...value, + name: event.target.value + })) + } + name="name" + error={!!getFieldError(errors, "name")} + helperText={getFieldError(errors, "name")} + /> + + + {mutationErrors.length > 0 && ( + <> + + {mutationErrors.map(err => ( + + {err} + + ))} + + )} + + + + + {i18n.t("Submit", { context: "button" })} + + ); }; diff --git a/src/navigation/components/MenuList/MenuList.tsx b/src/navigation/components/MenuList/MenuList.tsx index 12d802106..75c89ef23 100644 --- a/src/navigation/components/MenuList/MenuList.tsx +++ b/src/navigation/components/MenuList/MenuList.tsx @@ -18,9 +18,9 @@ import IconButtonTableCell from "@saleor/components/IconButtonTableCell"; import Skeleton from "@saleor/components/Skeleton"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; -import { maybe, renderCollection } from "../../../misc"; -import { ListActions, ListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { maybe, renderCollection } from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; import { MenuList_menus_edges_node } from "../../types/MenuList"; export interface MenuListProps extends ListProps, ListActions { @@ -44,15 +44,20 @@ const styles = (theme: Theme) => cursor: "pointer" } }); + +const numberOfColumns = 4; + const MenuList = withStyles(styles, { name: "MenuList" })( ({ classes, + settings, disabled, isChecked, menus, onDelete, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick, pageInfo, selected, @@ -63,6 +68,7 @@ const MenuList = withStyles(styles, { name: "MenuList" })(
toggle(menu.id)} /> @@ -130,7 +139,9 @@ const MenuList = withStyles(styles, { name: "MenuList" })( }, () => ( - {i18n.t("No menus found")} + + {i18n.t("No menus found")} + ) )} diff --git a/src/navigation/components/MenuListPage/MenuListPage.tsx b/src/navigation/components/MenuListPage/MenuListPage.tsx index 8e6b98d56..04f0f5926 100644 --- a/src/navigation/components/MenuListPage/MenuListPage.tsx +++ b/src/navigation/components/MenuListPage/MenuListPage.tsx @@ -5,8 +5,8 @@ import React from "react"; import AppHeader from "@saleor/components/AppHeader"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import i18n from "../../../i18n"; -import { ListActions, PageListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { ListActions, PageListProps } from "@saleor/types"; import { MenuList_menus_edges_node } from "../../types/MenuList"; import MenuList from "../MenuList"; diff --git a/src/navigation/urls.ts b/src/navigation/urls.ts index f0f791e61..0d70cf491 100644 --- a/src/navigation/urls.ts +++ b/src/navigation/urls.ts @@ -1,5 +1,6 @@ import { stringify as stringifyQs } from "qs"; import urlJoin from "url-join"; + import { BulkAction, Dialog, Pagination, SingleAction } from "../types"; export const navigationSection = "/navigation"; diff --git a/src/navigation/views/MenuDetails/index.tsx b/src/navigation/views/MenuDetails/index.tsx index c26d39343..1bc722399 100644 --- a/src/navigation/views/MenuDetails/index.tsx +++ b/src/navigation/views/MenuDetails/index.tsx @@ -44,6 +44,7 @@ import { } from "./successHandlers"; import { getInitialDisplayValue, + getMenuItemCreateInputData, getMenuItemInputData, getMoves, getRemoveIds @@ -262,10 +263,10 @@ const MenuDetails: React.FC = ({ id, params }) => { data: MenuItemDialogFormData ) => { const variables: MenuItemCreateVariables = { - input: { - menu: id, - ...getMenuItemInputData(data) - } + input: getMenuItemCreateInputData( + id, + data + ) }; menuItemCreate({ variables }); @@ -286,6 +287,12 @@ const MenuDetails: React.FC = ({ id, params }) => { open={params.action === "add-item"} categories={categories} collections={collections} + errors={maybe( + () => + menuItemCreateOpts.data + .menuItemCreate.errors, + [] + )} pages={pages} loading={ categorySearch.result.loading || @@ -355,6 +362,12 @@ const MenuDetails: React.FC = ({ id, params }) => { open={params.action === "edit-item"} categories={categories} collections={collections} + errors={maybe( + () => + menuItemUpdateOpts.data + .menuItemUpdate.errors, + [] + )} pages={pages} initial={initialFormData} initialDisplayValue={getInitialDisplayValue( diff --git a/src/navigation/views/MenuDetails/utils.ts b/src/navigation/views/MenuDetails/utils.ts index 188bd357b..1ad873304 100644 --- a/src/navigation/views/MenuDetails/utils.ts +++ b/src/navigation/views/MenuDetails/utils.ts @@ -1,4 +1,8 @@ -import { MenuItemInput, MenuItemMoveInput } from "../../../types/globalTypes"; +import { + MenuItemCreateInput, + MenuItemInput, + MenuItemMoveInput +} from "../../../types/globalTypes"; import { MenuDetailsSubmitData } from "../../components/MenuDetailsPage"; import { MenuItemDialogFormData } from "../../components/MenuItemDialog"; import { unknownTypeError } from "../../components/MenuItems"; @@ -34,6 +38,38 @@ export function getMenuItemInputData( return variables; } +export function getMenuItemCreateInputData( + menu: string, + data: MenuItemDialogFormData +): MenuItemCreateInput { + const variables: MenuItemCreateInput = { + menu, + name: data.name + }; + switch (data.type) { + case "category": + variables.category = data.id; + break; + + case "collection": + variables.collection = data.id; + break; + + case "page": + variables.page = data.id; + break; + + case "link": + variables.url = data.id; + break; + + default: + throw unknownTypeError; + } + + return variables; +} + export function getInitialDisplayValue(item: MenuDetails_menu_items): string { if (!item) { return "..."; @@ -47,7 +83,7 @@ export function getInitialDisplayValue(item: MenuDetails_menu_items): string { } else if (item.url) { return item.url; } else { - throw unknownTypeError; + return ""; } } diff --git a/src/navigation/views/MenuList.tsx b/src/navigation/views/MenuList.tsx index 35f05d79c..fbde8d36f 100644 --- a/src/navigation/views/MenuList.tsx +++ b/src/navigation/views/MenuList.tsx @@ -3,16 +3,17 @@ import DialogContentText from "@material-ui/core/DialogContentText"; import React from "react"; import ActionDialog from "@saleor/components/ActionDialog"; +import { configurationMenuUrl } from "@saleor/configuration"; import useBulkActions from "@saleor/hooks/useBulkActions"; +import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; -import { PAGINATE_BY } from "../../config"; -import { configurationMenuUrl } from "../../configuration"; -import i18n from "../../i18n"; -import { getMutationState, maybe } from "../../misc"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; import MenuCreateDialog from "../components/MenuCreateDialog"; import MenuListPage from "../components/MenuListPage"; import { @@ -36,6 +37,9 @@ const MenuList: React.FC = ({ params }) => { const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); + const { updateListSettings, settings } = useListSettings( + ListViews.NAVIGATION_LIST + ); const closeModal = () => navigate( @@ -48,7 +52,7 @@ const MenuList: React.FC = ({ params }) => { true ); - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); return ( @@ -129,6 +133,7 @@ const MenuList: React.FC = ({ params }) => { menus={maybe(() => data.menus.edges.map(edge => edge.node) )} + settings={settings} onAdd={() => navigate( menuListUrl({ @@ -147,6 +152,7 @@ const MenuList: React.FC = ({ params }) => { } onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(menuUrl(id))} pageInfo={pageInfo} isChecked={isSelected} diff --git a/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.tsx b/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.tsx index fa5eb177f..c351178cb 100644 --- a/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.tsx +++ b/src/orders/components/OrderAddressEditDialog/OrderAddressEditDialog.tsx @@ -11,6 +11,9 @@ import ConfirmButton, { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Form from "@saleor/components/Form"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { maybe } from "@saleor/misc"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import { AddressTypeInput } from "../../../customers/types"; import i18n from "../../../i18n"; import { UserError } from "../../../types"; @@ -48,43 +51,69 @@ const OrderAddressEditDialog = withStyles(styles, { countries, onClose, onConfirm - }: OrderAddressEditDialogProps) => ( - -
- {({ change, data, errors, submit }) => ( - <> - - {variant === "billing" - ? i18n.t("Edit billing address", { context: "title" }) - : i18n.t("Edit shipping address", { context: "title" })} - - - - - - - - {i18n.t("Confirm", { context: "button" })} - - - - )} - -
- ) + }: OrderAddressEditDialogProps) => { + const [countryDisplayName, setCountryDisplayName] = useStateFromProps( + maybe( + () => countries.find(country => address.country === country.code).label + ) + ); + const countryChoices = countries.map(country => ({ + label: country.label, + value: country.code + })); + + return ( + +
+ {({ change, data, errors, submit }) => { + const handleCountrySelect = createSingleAutocompleteSelectHandler( + change, + setCountryDisplayName, + countryChoices + ); + + return ( + <> + + {variant === "billing" + ? i18n.t("Edit billing address", { context: "title" }) + : i18n.t("Edit shipping address", { context: "title" })} + + + + + + + + {i18n.t("Confirm", { context: "button" })} + + + + ); + }} + +
+ ); + } ); OrderAddressEditDialog.displayName = "OrderAddressEditDialog"; export default OrderAddressEditDialog; diff --git a/src/orders/components/OrderCustomer/OrderCustomer.tsx b/src/orders/components/OrderCustomer/OrderCustomer.tsx index ea42bf8f7..c2319a911 100644 --- a/src/orders/components/OrderCustomer/OrderCustomer.tsx +++ b/src/orders/components/OrderCustomer/OrderCustomer.tsx @@ -17,6 +17,8 @@ import Hr from "@saleor/components/Hr"; import Link from "@saleor/components/Link"; import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField"; import Skeleton from "@saleor/components/Skeleton"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import { SearchCustomers_customers_edges_node } from "../../../containers/SearchCustomers/types/SearchCustomers"; import { customerUrl } from "../../../customers/urls"; import i18n from "../../../i18n"; @@ -72,12 +74,17 @@ const OrderCustomer = withStyles(styles, { name: "OrderCustomer" })( onProfileView, onShippingAddressEdit }: OrderCustomerProps) => { + const user = maybe(() => order.user); + + const [userDisplayName, setUserDisplayName] = useStateFromProps( + maybe(() => user.email, "") + ); const [isInEditMode, setEditModeStatus] = React.useState(false); const toggleEditMode = () => setEditModeStatus(!isInEditMode); const billingAddress = maybe(() => order.billingAddress); const shippingAddress = maybe(() => order.shippingAddress); - const user = maybe(() => order.user); + return ( ) : isInEditMode && canEditCustomer ? ( -
+ {({ change, data }) => { const handleChange = (event: React.ChangeEvent) => { change(event); + const value = event.target.value; + onCustomerEdit({ - [event.target.value.value.includes("@") - ? "userEmail" - : "user"]: event.target.value.value + [value.includes("@") ? "userEmail" : "user"]: value }); toggleEditMode(); }; + const userChoices = maybe(() => users, []).map(user => ({ + label: user.email, + value: user.id + })); + const handleUserChange = createSingleAutocompleteSelectHandler( + handleChange, + setUserDisplayName, + userChoices + ); return ( users, []).map(user => ({ - label: user.email, - value: user.id - }))} + allowCustomValues={true} + choices={userChoices} + displayValue={userDisplayName} fetchChoices={fetchUsers} loading={loading} placeholder={i18n.t("Search Customers")} - onChange={handleChange} + onChange={handleUserChange} name="query" value={data.query} /> diff --git a/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx b/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx index 3cd9804bb..7000c2289 100644 --- a/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx +++ b/src/orders/components/OrderCustomerEditDialog/OrderCustomerEditDialog.tsx @@ -38,10 +38,8 @@ const styles = (theme: Theme) => interface OrderCustomerEditDialogProps extends WithStyles { confirmButtonState: ConfirmButtonTransitionState; open: boolean; - user?: { - label: string; - value: string; - }; + user: string; + userDisplayValue: string; users?: Array<{ id: string; email: string; @@ -62,6 +60,7 @@ const OrderCustomerEditDialog = withStyles(styles, { open, loading, user, + userDisplayValue, users, fetchUsers, onChange, @@ -81,8 +80,9 @@ const OrderCustomerEditDialog = withStyles(styles, { createStyles({ - iconCell: { + colAction: { "&:last-child": { paddingRight: 0 }, width: 48 + theme.spacing.unit / 2 }, + colName: { + width: "auto" + }, + colNameLabel: { + marginLeft: AVATAR_MARGIN + }, + colPrice: { + textAlign: "right", + width: 150 + }, + colQuantity: { + textAlign: "right", + width: 80 + }, + colTotal: { + textAlign: "right", + width: 150 + }, quantityField: { "& input": { padding: "12px 12px 10px", @@ -43,8 +63,8 @@ const styles = (theme: Theme) => }, width: 60 }, - textRight: { - textAlign: "right" + table: { + tableLayout: "fixed" } }); @@ -63,23 +83,25 @@ const OrderDraftDetailsProducts = withStyles(styles, { onOrderLineChange, onOrderLineRemove }: OrderDraftDetailsProductsProps) => ( -
+
{maybe(() => !!lines.length) && ( - - {i18n.t("Product", { context: "table header" })} + + + {i18n.t("Product", { context: "table header" })} + - + {i18n.t("Quantity", { context: "table header" })} - + {i18n.t("Price", { context: "table header" })} - + {i18n.t("Total", { context: "table header" })} - + )} @@ -93,8 +115,10 @@ const OrderDraftDetailsProducts = withStyles(styles, { ) : ( renderCollection(lines, line => ( - line.thumbnail.url)} /> - + line.thumbnail.url)} + > {maybe(() => line.productName && line.productSku) ? ( <> {line.productName} @@ -103,8 +127,8 @@ const OrderDraftDetailsProducts = withStyles(styles, { ) : ( )} - - + + {maybe(() => line.quantity) ? ( )} - + {maybe(() => line.unitPrice.net) ? ( ) : ( )} - + {maybe(() => line.unitPrice.net && line.quantity) ? ( )} - + onOrderLineRemove(line.id)}> diff --git a/src/orders/components/OrderDraftList/OrderDraftList.tsx b/src/orders/components/OrderDraftList/OrderDraftList.tsx index e6ab22d0b..8ebfde633 100644 --- a/src/orders/components/OrderDraftList/OrderDraftList.tsx +++ b/src/orders/components/OrderDraftList/OrderDraftList.tsx @@ -17,14 +17,14 @@ import Money from "@saleor/components/Money"; import Skeleton from "@saleor/components/Skeleton"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; +import i18n from "@saleor/i18n"; import { maybe, renderCollection, transformOrderStatus, transformPaymentStatus -} from "../../../misc"; -import { ListActions, ListProps } from "../../../types"; +} from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; import { OrderDraftList_draftOrders_edges_node } from "../../types/OrderDraftList"; const styles = (theme: Theme) => @@ -59,14 +59,18 @@ interface OrderDraftListProps orders: OrderDraftList_draftOrders_edges_node[]; } +const numberOfColumns = 5; + export const OrderDraftList = withStyles(styles, { name: "OrderDraftList" })( ({ classes, disabled, + settings, orders, pageInfo, onPreviousPage, onNextPage, + onUpdateListSettings, onRowClick, isChecked, selected, @@ -84,6 +88,7 @@ export const OrderDraftList = withStyles(styles, { name: "OrderDraftList" })( return (
toggle(order.id)} /> @@ -176,7 +184,9 @@ export const OrderDraftList = withStyles(styles, { name: "OrderDraftList" })( }, () => ( - {i18n.t("No orders found")} + + {i18n.t("No orders found")} + ) )} diff --git a/src/orders/components/OrderDraftListPage/OrderDraftListPage.tsx b/src/orders/components/OrderDraftListPage/OrderDraftListPage.tsx index 09b7063c2..a172986a6 100644 --- a/src/orders/components/OrderDraftListPage/OrderDraftListPage.tsx +++ b/src/orders/components/OrderDraftListPage/OrderDraftListPage.tsx @@ -5,8 +5,8 @@ import React from "react"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import i18n from "../../../i18n"; -import { ListActions, PageListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { ListActions, PageListProps } from "@saleor/types"; import { OrderDraftList_draftOrders_edges_node } from "../../types/OrderDraftList"; import OrderDraftList from "../OrderDraftList"; diff --git a/src/orders/components/OrderFulfillment/OrderFulfillment.tsx b/src/orders/components/OrderFulfillment/OrderFulfillment.tsx index ee4d4241d..041c4acb3 100644 --- a/src/orders/components/OrderFulfillment/OrderFulfillment.tsx +++ b/src/orders/components/OrderFulfillment/OrderFulfillment.tsx @@ -20,7 +20,9 @@ import CardTitle from "@saleor/components/CardTitle"; import Money from "@saleor/components/Money"; import Skeleton from "@saleor/components/Skeleton"; import StatusLabel from "@saleor/components/StatusLabel"; -import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import TableCellAvatar, { + AVATAR_MARGIN +} from "@saleor/components/TableCellAvatar"; import i18n from "../../../i18n"; import { maybe, renderCollection } from "../../../misc"; import { FulfillmentStatus } from "../../../types/globalTypes"; @@ -31,6 +33,25 @@ const styles = (theme: Theme) => clickableRow: { cursor: "pointer" }, + colName: { + width: "auto" + }, + colNameLabel: { + marginLeft: AVATAR_MARGIN + }, + colPrice: { + textAlign: "right", + width: 120 + }, + colQuantity: { + textAlign: "center", + width: 120 + }, + colTotal: { + textAlign: "right", + width: 120 + }, + orderNumber: { display: "inline", marginLeft: theme.spacing.unit @@ -38,14 +59,8 @@ const styles = (theme: Theme) => statusBar: { paddingTop: 0 }, - textCenter: { - textAlign: "center" - }, - textRight: { - textAlign: "right" - }, - wideCell: { - width: "50%" + table: { + tableLayout: "fixed" } }); @@ -56,6 +71,8 @@ interface OrderFulfillmentProps extends WithStyles { onTrackingCodeAdd: () => void; } +const numberOfColumns = 3; + const OrderFulfillment = withStyles(styles, { name: "OrderFulfillment" })( ({ classes, @@ -115,19 +132,21 @@ const OrderFulfillment = withStyles(styles, { name: "OrderFulfillment" })( ) } /> -
+
- - {i18n.t("Product")} + + + {i18n.t("Product")} + - + {i18n.t("Quantity")} - + {i18n.t("Price")} - + {i18n.t("Total")} @@ -140,22 +159,22 @@ const OrderFulfillment = withStyles(styles, { name: "OrderFulfillment" })( key={maybe(() => line.id)} > line.orderLine.thumbnail.url)} - /> - + > {maybe(() => line.orderLine.productName) || } - - + + {maybe(() => line.quantity) || } - + {maybe(() => line.orderLine.unitPrice.gross) ? ( ) : ( )} - + {maybe( () => line.quantity * line.orderLine.unitPrice.gross.amount ) ? ( @@ -174,7 +193,7 @@ const OrderFulfillment = withStyles(styles, { name: "OrderFulfillment" })( ))} {maybe(() => fulfillment.trackingNumber) && ( - + {i18n.t("Tracking Number: {{ trackingNumber }}", { trackingNumber: fulfillment.trackingNumber })} diff --git a/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx b/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx index 92657cdb8..8be5156cc 100644 --- a/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx +++ b/src/orders/components/OrderFulfillmentDialog/OrderFulfillmentDialog.tsx @@ -22,7 +22,9 @@ import ConfirmButton, { } from "@saleor/components/ConfirmButton"; import Form from "@saleor/components/Form"; import { FormSpacer } from "@saleor/components/FormSpacer"; -import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import TableCellAvatar, { + AVATAR_MARGIN +} from "@saleor/components/TableCellAvatar"; import i18n from "../../../i18n"; import { maybe } from "../../../misc"; import { OrderDetails_order_lines } from "../../types/OrderDetails"; @@ -34,23 +36,32 @@ export interface FormData { const styles = (theme: Theme) => createStyles({ - avatarCell: { - paddingLeft: theme.spacing.unit * 2, - paddingRight: theme.spacing.unit * 3, - width: theme.spacing.unit * 5 + colName: { + width: "auto" + }, + colNameLabel: { + marginLeft: AVATAR_MARGIN + }, + colQuantity: { + textAlign: "right", + width: 150 + }, + colQuantityContent: { + alignItems: "center", + display: "inline-flex" + }, + colSku: { + width: 120 }, quantityInput: { width: "4rem" }, - quantityInputContainer: { - paddingRight: theme.spacing.unit, - textAlign: "right" as "right" - }, remainingQuantity: { - paddingBottom: 4 + marginLeft: theme.spacing.unit, + paddingTop: 14 }, - textRight: { - textAlign: "right" as "right" + table: { + tableLayout: "fixed" } }); @@ -104,13 +115,18 @@ const OrderFulfillmentDialog = withStyles(styles, { return ( <> {i18n.t("Fulfill products")} -
+
- - {i18n.t("Product name")} - {i18n.t("SKU")} - + + + {i18n.t("Product name")} + + + + {i18n.t("SKU")} + + {i18n.t("Quantity")} @@ -122,28 +138,34 @@ const OrderFulfillmentDialog = withStyles(styles, { return ( product.thumbnail.url)} - /> - {product.productName} - {product.productSku} - - - handleQuantityChange(productIndex, event) - } - error={remainingQuantity < data.lines[productIndex]} - /> + > + {product.productName} + + + {product.productSku} - -
- / {remainingQuantity} + +
+ + handleQuantityChange(productIndex, event) + } + error={ + remainingQuantity < data.lines[productIndex] + } + /> +
+ / {remainingQuantity} +
diff --git a/src/orders/components/OrderList/OrderList.tsx b/src/orders/components/OrderList/OrderList.tsx index b64b8c872..c0b772ab7 100644 --- a/src/orders/components/OrderList/OrderList.tsx +++ b/src/orders/components/OrderList/OrderList.tsx @@ -18,14 +18,14 @@ import Skeleton from "@saleor/components/Skeleton"; import StatusLabel from "@saleor/components/StatusLabel"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; +import i18n from "@saleor/i18n"; import { maybe, renderCollection, transformOrderStatus, transformPaymentStatus -} from "../../../misc"; -import { ListActions, ListProps } from "../../../types"; +} from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; import { OrderList_orders_edges_node } from "../../types/OrderList"; const styles = (theme: Theme) => @@ -66,14 +66,18 @@ interface OrderListProps orders: OrderList_orders_edges_node[]; } +const numberOfColumns = 7; + export const OrderList = withStyles(styles, { name: "OrderList" })( ({ classes, disabled, + settings, orders, pageInfo, onPreviousPage, onNextPage, + onUpdateListSettings, onRowClick, isChecked, selected, @@ -91,6 +95,7 @@ export const OrderList = withStyles(styles, { name: "OrderList" })( return (
toggle(order.id)} /> @@ -211,7 +219,9 @@ export const OrderList = withStyles(styles, { name: "OrderList" })( }, () => ( - {i18n.t("No orders found")} + + {i18n.t("No orders found")} + ) )} diff --git a/src/orders/components/OrderListFilter/OrderListFilter.tsx b/src/orders/components/OrderListFilter/OrderListFilter.tsx index 8651b6b0a..a76489479 100644 --- a/src/orders/components/OrderListFilter/OrderListFilter.tsx +++ b/src/orders/components/OrderListFilter/OrderListFilter.tsx @@ -1,4 +1,4 @@ -import * as moment from "moment-timezone"; +import moment from "moment-timezone"; import React from "react"; import { DateContext } from "@saleor/components/Date/DateContext"; diff --git a/src/orders/components/OrderPaymentDialog/OrderPaymentDialog.tsx b/src/orders/components/OrderPaymentDialog/OrderPaymentDialog.tsx index bdb77880d..5e9f8b068 100644 --- a/src/orders/components/OrderPaymentDialog/OrderPaymentDialog.tsx +++ b/src/orders/components/OrderPaymentDialog/OrderPaymentDialog.tsx @@ -72,9 +72,9 @@ const OrderPaymentDialog: React.StatelessComponent = ({ transitionState={confirmButtonState} color="primary" variant="contained" - onClick={data => { + onClick={() => { onClose(); - submit(data); + submit(); }} > {i18n.t("Confirm", { context: "button" })} diff --git a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx index 1acc3292a..7b5f4f557 100644 --- a/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx +++ b/src/orders/components/OrderProductAddDialog/OrderProductAddDialog.tsx @@ -16,34 +16,29 @@ import TableCell from "@material-ui/core/TableCell"; import TableRow from "@material-ui/core/TableRow"; import TextField from "@material-ui/core/TextField"; import React from "react"; -import * as InfiniteScroll from "react-infinite-scroller"; +import InfiniteScroll from "react-infinite-scroller"; import Checkbox from "@saleor/components/Checkbox"; import ConfirmButton, { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -import Form from "@saleor/components/Form"; import Money from "@saleor/components/Money"; import TableCellAvatar from "@saleor/components/TableCellAvatar"; -import i18n from "../../../i18n"; -import { maybe, renderCollection } from "../../../misc"; +import useSearchQuery from "@saleor/hooks/useSearchQuery"; +import i18n from "@saleor/i18n"; +import { maybe, renderCollection } from "@saleor/misc"; +import { FetchMoreProps } from "@saleor/types"; import { SearchOrderVariant_products_edges_node, SearchOrderVariant_products_edges_node_variants } from "../../types/SearchOrderVariant"; -export interface FormData { - variants: SearchOrderVariant_products_edges_node_variants[]; - query: string; -} - const styles = (theme: Theme) => createStyles({ avatar: { paddingLeft: 0 }, content: { - maxHeight: 600, overflowY: "scroll" }, grayText: { @@ -76,31 +71,26 @@ const styles = (theme: Theme) => } }); -interface OrderProductAddDialogProps extends WithStyles { +type SetVariantsAction = ( + data: SearchOrderVariant_products_edges_node_variants[] +) => void; + +interface OrderProductAddDialogProps extends FetchMoreProps { confirmButtonState: ConfirmButtonTransitionState; open: boolean; products: SearchOrderVariant_products_edges_node[]; - loading: boolean; - hasMore: boolean; onClose: () => void; - onFetch: (value: string) => void; - onFetchMore: () => void; - onSubmit: (data: FormData) => void; + onSubmit: (data: SearchOrderVariant_products_edges_node_variants[]) => void; } -const initialForm: FormData = { - query: "", - variants: [] -}; - function hasAllVariantsSelected( productVariants: SearchOrderVariant_products_edges_node_variants[], - selectedVariants: SearchOrderVariant_products_edges_node_variants[] + selectedVariantsToProductsMap: SearchOrderVariant_products_edges_node_variants[] ): boolean { return productVariants.reduce( (acc, productVariant) => acc && - !!selectedVariants.find( + !!selectedVariantsToProductsMap.find( selectedVariant => selectedVariant.id === productVariant.id ), true @@ -109,13 +99,53 @@ function hasAllVariantsSelected( function isVariantSelected( variant: SearchOrderVariant_products_edges_node_variants, - selectedVariants: SearchOrderVariant_products_edges_node_variants[] + selectedVariantsToProductsMap: SearchOrderVariant_products_edges_node_variants[] ): boolean { - return !!selectedVariants.find( + return !!selectedVariantsToProductsMap.find( selectedVariant => selectedVariant.id === variant.id ); } +const onProductAdd = ( + product: SearchOrderVariant_products_edges_node, + productIndex: number, + productsWithAllVariantsSelected: boolean[], + variants: SearchOrderVariant_products_edges_node_variants[], + setVariants: SetVariantsAction +) => + productsWithAllVariantsSelected[productIndex] + ? setVariants( + variants.filter( + selectedVariant => + !product.variants.find( + productVariant => productVariant.id === selectedVariant.id + ) + ) + ) + : setVariants([ + ...variants, + ...product.variants.filter( + productVariant => + !variants.find( + selectedVariant => selectedVariant.id === productVariant.id + ) + ) + ]); + +const onVariantAdd = ( + variant: SearchOrderVariant_products_edges_node_variants, + variantIndex: number, + productIndex: number, + variants: SearchOrderVariant_products_edges_node_variants[], + selectedVariantsToProductsMap: boolean[][], + setVariants: SetVariantsAction +) => + selectedVariantsToProductsMap[productIndex][variantIndex] + ? setVariants( + variants.filter(selectedVariant => selectedVariant.id !== variant.id) + ) + : setVariants([...variants, variant]); + const OrderProductAddDialog = withStyles(styles, { name: "OrderProductAddDialog" })( @@ -130,220 +160,173 @@ const OrderProductAddDialog = withStyles(styles, { onFetchMore, onClose, onSubmit - }: OrderProductAddDialogProps) => ( - - - {({ data, change }) => { - const selectedVariants = products - ? products.map(product => - product.variants.map(variant => - isVariantSelected(variant, data.variants) - ) - ) - : []; - const selectedProducts = products - ? products.map(product => - hasAllVariantsSelected(product.variants, data.variants) - ) - : []; + }: OrderProductAddDialogProps & WithStyles) => { + const [query, onQueryChange] = useSearchQuery(onFetch); + const [variants, setVariants] = React.useState< + SearchOrderVariant_products_edges_node_variants[] + >([]); - const onProductAdd = ( - product: SearchOrderVariant_products_edges_node, - productIndex: number - ) => - selectedProducts[productIndex] - ? change({ - target: { - name: "variants", - value: data.variants.filter( - selectedVariant => - !product.variants.find( - productVariant => - productVariant.id === selectedVariant.id - ) - ) - } - } as any) - : change({ - target: { - name: "variants", - value: [ - ...data.variants, - ...product.variants.filter( - productVariant => - !data.variants.find( - selectedVariant => - selectedVariant.id === productVariant.id - ) - ) - ] - } - } as any); - const onVariantAdd = ( - variant: SearchOrderVariant_products_edges_node_variants, - variantIndex: number, - productIndex: number - ) => - selectedVariants[productIndex][variantIndex] - ? change({ - target: { - name: "variants", - value: data.variants.filter( - selectedVariant => selectedVariant.id !== variant.id - ) - } - } as any) - : change({ - target: { - name: "variants", - value: [...data.variants, variant] - } - } as any); + const selectedVariantsToProductsMap = products + ? products.map(product => + product.variants.map(variant => isVariantSelected(variant, variants)) + ) + : []; + const productsWithAllVariantsSelected = products + ? products.map(product => + hasAllVariantsSelected(product.variants, variants) + ) + : []; - return ( - <> - {i18n.t("Add product")} - - change(event, () => onFetch(data.query))} - label={i18n.t("Search Products", { - context: "product search input label" - })} - placeholder={i18n.t( - "Search by product name, attribute, product type etc...", - { - context: "product search input placeholder" - } - )} - fullWidth - InputProps={{ - autoComplete: "off", - endAdornment: loading && - }} - /> - - - - - - } - threshold={10} - > -
- - {renderCollection( - products, - (product, productIndex) => ( - - - - - onProductAdd(product, productIndex) - } - /> - - product.thumbnail.url)} - /> - - {maybe(() => product.name)} - - - {maybe(() => product.variants, []).map( - (variant, variantIndex) => ( - - - - - onVariantAdd( - variant, - variantIndex, - productIndex - ) - } - /> - - -
{variant.name}
-
- {i18n.t("SKU {{ sku }}", { - sku: variant.sku - })} -
-
- - - -
+ const handleSubmit = () => onSubmit(variants); + + return ( + + {i18n.t("Add product")} + + + }} + /> + + + + + + } + threshold={10} + > +
+ + {renderCollection( + products, + (product, productIndex) => ( + + + + + onProductAdd( + product, + productIndex, + productsWithAllVariantsSelected, + variants, + setVariants ) - )} - - ), - () => ( - - - {i18n.t("No products matching given query")} + } + /> + + product.thumbnail.url)} + /> + + {maybe(() => product.name)} + + + {maybe(() => product.variants, []).map( + (variant, variantIndex) => ( + + + + + onVariantAdd( + variant, + variantIndex, + productIndex, + variants, + selectedVariantsToProductsMap, + setVariants + ) + } + /> + + +
{variant.name}
+
+ {i18n.t("SKU {{ sku }}", { + sku: variant.sku + })} +
+
+ +
) )} -
-
- - - - - - {i18n.t("Confirm", { context: "button" })} - - - - ); - }} - - - ) + + ), + () => ( + + + {i18n.t("No products matching given query")} + + + ) + )} + + + + + + + + {i18n.t("Confirm", { context: "button" })} + + + + ); + } ); OrderProductAddDialog.displayName = "OrderProductAddDialog"; export default OrderProductAddDialog; diff --git a/src/orders/components/OrderUnfulfilledItems/OrderUnfulfilledItems.tsx b/src/orders/components/OrderUnfulfilledItems/OrderUnfulfilledItems.tsx index 7a20e34b0..ade726e67 100644 --- a/src/orders/components/OrderUnfulfilledItems/OrderUnfulfilledItems.tsx +++ b/src/orders/components/OrderUnfulfilledItems/OrderUnfulfilledItems.tsx @@ -13,7 +13,9 @@ import CardTitle from "@saleor/components/CardTitle"; import Money from "@saleor/components/Money"; import Skeleton from "@saleor/components/Skeleton"; import StatusLabel from "@saleor/components/StatusLabel"; -import TableCellAvatar from "@saleor/components/TableCellAvatar"; +import TableCellAvatar, { + AVATAR_MARGIN +} from "@saleor/components/TableCellAvatar"; import i18n from "../../../i18n"; import { maybe } from "../../../misc"; import { OrderDetails_order_lines } from "../../types/OrderDetails"; @@ -22,14 +24,29 @@ const styles = createStyles({ clickableRow: { cursor: "pointer" }, - textCenter: { - textAlign: "center" + colName: { + width: "auto" }, - textRight: { - textAlign: "right" + colNameLabel: { + marginLeft: AVATAR_MARGIN }, - wideCell: { - width: "50%" + colPrice: { + textAlign: "right", + width: 120 + }, + colQuantity: { + textAlign: "center", + width: 120 + }, + colTotal: { + textAlign: "right", + width: 120 + }, + statusBar: { + paddingTop: 0 + }, + table: { + tableLayout: "fixed" } }); @@ -55,17 +72,17 @@ const OrderUnfulfilledItems = withStyles(styles, { /> } /> - +
- - {i18n.t("Product")} + + {i18n.t("Product")} - + {i18n.t("Quantity")} - {i18n.t("Price")} - {i18n.t("Total")} + {i18n.t("Price")} + {i18n.t("Total")} @@ -75,23 +92,25 @@ const OrderUnfulfilledItems = withStyles(styles, { hover={!!line} key={maybe(() => line.id)} > - line.thumbnail.url)} /> - + line.thumbnail.url)} + > {maybe(() => line.productName) || } - - + + {maybe(() => line.quantity - line.quantityFulfilled) || ( )} - + {maybe(() => line.unitPrice.gross) ? ( ) : ( )} - + {maybe( () => (line.quantity - line.quantityFulfilled) * diff --git a/src/orders/views/OrderDetails/index.tsx b/src/orders/views/OrderDetails/index.tsx index a1cb70178..8a4052c42 100644 --- a/src/orders/views/OrderDetails/index.tsx +++ b/src/orders/views/OrderDetails/index.tsx @@ -567,15 +567,13 @@ export const OrderDetails: React.StatelessComponent = ({ onClose={closeModal} onFetch={variantSearch} onFetchMore={fetchMore} - onSubmit={formData => + onSubmit={variants => orderLinesAdd.mutate({ id, - input: formData.variants.map( - variant => ({ - quantity: 1, - variantId: variant.id - }) - ) + input: variants.map(variant => ({ + quantity: 1, + variantId: variant.id + })) }) } /> @@ -608,14 +606,11 @@ export const OrderDetails: React.StatelessComponent = ({ open={params.action === "edit-shipping-address"} variant="shipping" onClose={closeModal} - onConfirm={variables => + onConfirm={shippingAddress => orderUpdate.mutate({ id, input: { - shippingAddress: { - ...variables, - country: variables.country.value - } + shippingAddress } }) } @@ -644,14 +639,11 @@ export const OrderDetails: React.StatelessComponent = ({ open={params.action === "edit-billing-address"} variant="billing" onClose={closeModal} - onConfirm={variables => + onConfirm={billingAddress => orderUpdate.mutate({ id, input: { - billingAddress: { - ...variables, - country: variables.country.value - } + billingAddress } }) } diff --git a/src/orders/views/OrderDraftList.tsx b/src/orders/views/OrderDraftList.tsx index 17e9ebf2a..129ba3fcf 100644 --- a/src/orders/views/OrderDraftList.tsx +++ b/src/orders/views/OrderDraftList.tsx @@ -5,14 +5,15 @@ import React from "react"; import ActionDialog from "@saleor/components/ActionDialog"; import useBulkActions from "@saleor/hooks/useBulkActions"; +import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; -import { PAGINATE_BY } from "../../config"; -import i18n from "../../i18n"; -import { getMutationState, maybe } from "../../misc"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; import OrderDraftListPage from "../components/OrderDraftListPage"; import { TypedOrderDraftBulkCancelMutation, @@ -40,6 +41,9 @@ export const OrderDraftList: React.StatelessComponent = ({ const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); + const { updateListSettings, settings } = useListSettings( + ListViews.DRAFT_LIST + ); const closeModal = () => navigate( @@ -57,7 +61,7 @@ export const OrderDraftList: React.StatelessComponent = ({ navigate(orderUrl(data.draftOrderCreate.order.id)); }; - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); return ( @@ -106,6 +110,7 @@ export const OrderDraftList: React.StatelessComponent = ({ <> data.draftOrders.edges.map(edge => edge.node) )} @@ -113,6 +118,7 @@ export const OrderDraftList: React.StatelessComponent = ({ onAdd={createOrder} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(orderUrl(id))} isChecked={isSelected} selected={listElements.length} diff --git a/src/orders/views/OrderList/OrderList.tsx b/src/orders/views/OrderList/OrderList.tsx index 7c40ef420..c099b6815 100644 --- a/src/orders/views/OrderList/OrderList.tsx +++ b/src/orders/views/OrderList/OrderList.tsx @@ -5,9 +5,9 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import SaveFilterTabDialog, { SaveFilterTabDialogFormData } from "@saleor/components/SaveFilterTabDialog"; -import { PAGINATE_BY } from "@saleor/config"; import useBulkActions from "@saleor/hooks/useBulkActions"; import useDateLocalize from "@saleor/hooks/useDateLocalize"; +import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { @@ -16,6 +16,7 @@ import usePaginator, { import useShop from "@saleor/hooks/useShop"; import i18n from "@saleor/i18n"; import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; import OrderBulkCancelDialog from "../../components/OrderBulkCancelDialog"; import OrderListPage from "../../components/OrderListPage/OrderListPage"; import { @@ -58,6 +59,9 @@ export const OrderList: React.StatelessComponent = ({ const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); + const { updateListSettings, settings } = useListSettings( + ListViews.ORDER_LIST + ); const tabs = getFilterTabs(); @@ -124,7 +128,7 @@ export const OrderList: React.StatelessComponent = ({ handleTabChange(tabs.length + 1); }; - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); const handleCreateOrderCreateSuccess = (data: OrderDraftCreate) => { @@ -139,7 +143,7 @@ export const OrderList: React.StatelessComponent = ({ ...paginationState, filter: getFilterVariables(params) }), - [params] + [params, settings.rowNumber] ); return ( @@ -186,6 +190,7 @@ export const OrderList: React.StatelessComponent = ({ <> = ({ onAdd={createOrder} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(orderUrl(id))} isChecked={isSelected} selected={listElements.length} diff --git a/src/pages/components/PageList/PageList.tsx b/src/pages/components/PageList/PageList.tsx index a02195c84..dc3ac0b36 100644 --- a/src/pages/components/PageList/PageList.tsx +++ b/src/pages/components/PageList/PageList.tsx @@ -17,9 +17,9 @@ import Skeleton from "@saleor/components/Skeleton"; import StatusLabel from "@saleor/components/StatusLabel"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; -import { maybe, renderCollection } from "../../../misc"; -import { ListActions, ListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { maybe, renderCollection } from "@saleor/misc"; +import { ListActions, ListProps } from "@saleor/types"; import { PageList_pages_edges_node } from "../../types/PageList"; export interface PageListProps extends ListProps, ListActions { @@ -44,14 +44,19 @@ const styles = (theme: Theme) => cursor: "pointer" } }); + +const numberOfColumns = 4; + const PageList = withStyles(styles, { name: "PageList" })( ({ classes, + settings, pages, disabled, onNextPage, pageInfo, onRowClick, + onUpdateListSettings, onPreviousPage, isChecked, selected, @@ -62,6 +67,7 @@ const PageList = withStyles(styles, { name: "PageList" })(
toggle(page.id)} /> @@ -138,7 +147,9 @@ const PageList = withStyles(styles, { name: "PageList" })( }, () => ( - {i18n.t("No pages found")} + + {i18n.t("No pages found")} + ) )} diff --git a/src/pages/components/PageListPage/PageListPage.tsx b/src/pages/components/PageListPage/PageListPage.tsx index d5666cb8c..282058a50 100644 --- a/src/pages/components/PageListPage/PageListPage.tsx +++ b/src/pages/components/PageListPage/PageListPage.tsx @@ -5,8 +5,8 @@ import React from "react"; import AppHeader from "@saleor/components/AppHeader"; import Container from "@saleor/components/Container"; import PageHeader from "@saleor/components/PageHeader"; -import i18n from "../../../i18n"; -import { ListActions, PageListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { ListActions, PageListProps } from "@saleor/types"; import { PageList_pages_edges_node } from "../../types/PageList"; import PageList from "../PageList/PageList"; @@ -17,11 +17,13 @@ export interface PageListPageProps extends PageListProps, ListActions { const PageListPage: React.StatelessComponent = ({ disabled, + settings, onAdd, onBack, onNextPage, onPreviousPage, onRowClick, + onUpdateListSettings, pageInfo, pages, isChecked, @@ -45,9 +47,11 @@ const PageListPage: React.StatelessComponent = ({ = ({ const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); - - const paginationState = createPaginationState(PAGINATE_BY, params); + const { updateListSettings, settings } = useListSettings( + ListViews.PAGES_LIST + ); + const paginationState = createPaginationState(settings.rowNumber, params); return ( @@ -115,6 +118,7 @@ export const PageList: React.StatelessComponent = ({ <> data.pages.edges.map(edge => edge.node) )} @@ -123,6 +127,7 @@ export const PageList: React.StatelessComponent = ({ onBack={() => navigate(configurationMenuUrl)} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(pageUrl(id))} toolbar={ <> diff --git a/src/productTypes/components/ProductTypeAttributes/ProductTypeAttributes.tsx b/src/productTypes/components/ProductTypeAttributes/ProductTypeAttributes.tsx index 68bec7865..99087212c 100644 --- a/src/productTypes/components/ProductTypeAttributes/ProductTypeAttributes.tsx +++ b/src/productTypes/components/ProductTypeAttributes/ProductTypeAttributes.tsx @@ -8,18 +8,23 @@ import { WithStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; -import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import DeleteIcon from "@material-ui/icons/Delete"; import React from "react"; import CardTitle from "@saleor/components/CardTitle"; +import Checkbox from "@saleor/components/Checkbox"; import Skeleton from "@saleor/components/Skeleton"; -import i18n from "../../../i18n"; -import { maybe, renderCollection } from "../../../misc"; -import { AttributeTypeEnum } from "../../../types/globalTypes"; +import { + SortableTableBody, + SortableTableRow +} from "@saleor/components/SortableTable"; +import TableHead from "@saleor/components/TableHead"; +import i18n from "@saleor/i18n"; +import { maybe, renderCollection, stopPropagation } from "@saleor/misc"; +import { ListActions, ReorderAction } from "@saleor/types"; +import { AttributeTypeEnum } from "@saleor/types/globalTypes"; import { ProductTypeDetails_productType_productAttributes, ProductTypeDetails_productType_variantAttributes @@ -27,11 +32,15 @@ import { const styles = (theme: Theme) => createStyles({ + colName: {}, + colSlug: { + width: 300 + }, iconCell: { "&:last-child": { paddingRight: 0 }, - width: 48 + theme.spacing.unit / 2 + width: 48 + theme.spacing.unit * 1.5 }, link: { cursor: "pointer" @@ -41,27 +50,38 @@ const styles = (theme: Theme) => } }); -interface ProductTypeAttributesProps extends WithStyles { +interface ProductTypeAttributesProps extends ListActions { attributes: | ProductTypeDetails_productType_productAttributes[] | ProductTypeDetails_productType_variantAttributes[]; - type: AttributeTypeEnum; - onAttributeAdd: (type: AttributeTypeEnum) => void; - onAttributeDelete: (id: string, event: React.MouseEvent) => void; - onAttributeUpdate: (id: string) => void; + disabled: boolean; + type: string; + onAttributeAssign: (type: AttributeTypeEnum) => void; + onAttributeClick: (id: string) => void; + onAttributeReorder: ReorderAction; + onAttributeUnassign: (id: string) => void; } +const numberOfColumns = 5; + const ProductTypeAttributes = withStyles(styles, { name: "ProductTypeAttributes" })( ({ attributes, classes, + disabled, + isChecked, + selected, + toggle, + toggleAll, + toolbar, type, - onAttributeAdd, - onAttributeDelete, - onAttributeUpdate - }: ProductTypeAttributesProps) => ( + onAttributeAssign, + onAttributeClick, + onAttributeReorder, + onAttributeUnassign + }: ProductTypeAttributesProps & WithStyles) => ( onAttributeAdd(type)} + onClick={() => onAttributeAssign(AttributeTypeEnum[type])} > - {i18n.t("Add attribute", { context: "button" })} + {i18n.t("Assign attribute", { context: "button" })} } />
- - - {i18n.t("Attribute name")} - - {i18n.t("Values")} - - - + + + {i18n.t("Attribute name")} + + {i18n.t("Slug")} + - + {renderCollection( attributes, - attribute => ( - onAttributeUpdate(attribute.id) - : undefined - } - key={maybe(() => attribute.id)} - > - - {maybe(() => attribute.name) ? attribute.name : } - - - {maybe(() => attribute.values) !== undefined ? ( - attribute.values.map(value => value.name).join(", ") - ) : ( - - )} - - - onAttributeDelete(attribute.id, event)} - > - - - - - ), + (attribute, attributeIndex) => { + const isSelected = attribute ? isChecked(attribute.id) : false; + + return ( + onAttributeClick(attribute.id) + : undefined + } + key={maybe(() => attribute.id)} + index={attributeIndex || 0} + > + + toggle(attribute.id)} + /> + + + {maybe(() => attribute.name) ? ( + attribute.name + ) : ( + + )} + + + {maybe(() => attribute.slug) ? ( + attribute.slug + ) : ( + + )} + + + + onAttributeUnassign(attribute.id) + )} + > + + + + + ); + }, () => ( - + {i18n.t("No attributes found")} ) )} - +
) diff --git a/src/productTypes/components/ProductTypeCreatePage/ProductTypeCreatePage.tsx b/src/productTypes/components/ProductTypeCreatePage/ProductTypeCreatePage.tsx index d59b23daf..10ce39d20 100644 --- a/src/productTypes/components/ProductTypeCreatePage/ProductTypeCreatePage.tsx +++ b/src/productTypes/components/ProductTypeCreatePage/ProductTypeCreatePage.tsx @@ -8,33 +8,53 @@ import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; -import i18n from "../../../i18n"; -import { TaxRateType, WeightUnitsEnum } from "../../../types/globalTypes"; +import { ChangeEvent, FormChange } from "@saleor/hooks/useForm"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import i18n from "@saleor/i18n"; +import { ProductTypeDetails_taxTypes } from "@saleor/productTypes/types/ProductTypeDetails"; +import { UserError } from "@saleor/types"; +import { WeightUnitsEnum } from "@saleor/types/globalTypes"; import ProductTypeDetails from "../ProductTypeDetails/ProductTypeDetails"; import ProductTypeShipping from "../ProductTypeShipping/ProductTypeShipping"; import ProductTypeTaxes from "../ProductTypeTaxes/ProductTypeTaxes"; export interface ProductTypeForm { - chargeTaxes: boolean; name: string; isShippingRequired: boolean; - taxRate: TaxRateType; + taxType: string; weight: number; } export interface ProductTypeCreatePageProps { - errors: Array<{ - field: string; - message: string; - }>; + errors: UserError[]; defaultWeightUnit: WeightUnitsEnum; disabled: boolean; pageTitle: string; saveButtonBarState: ConfirmButtonTransitionState; + taxTypes: ProductTypeDetails_taxTypes[]; onBack: () => void; onSubmit: (data: ProductTypeForm) => void; } +const formInitialData: ProductTypeForm = { + isShippingRequired: false, + name: "", + taxType: "", + weight: 0 +}; + +function handleTaxTypeChange( + event: ChangeEvent, + taxTypes: ProductTypeDetails_taxTypes[], + formChange: FormChange, + displayChange: (name: string) => void +) { + formChange(event); + displayChange( + taxTypes.find(taxType => taxType.taxCode === event.target.value).description + ); +} + const ProductTypeCreatePage: React.StatelessComponent< ProductTypeCreatePageProps > = ({ @@ -43,16 +63,12 @@ const ProductTypeCreatePage: React.StatelessComponent< errors, pageTitle, saveButtonBarState, + taxTypes, onBack, onSubmit }: ProductTypeCreatePageProps) => { - const formInitialData: ProductTypeForm = { - chargeTaxes: true, - isShippingRequired: false, - name: "", - taxRate: TaxRateType.STANDARD, - weight: 0 - }; + const [taxTypeDisplayName, setTaxTypeDisplayName] = useStateFromProps(""); + return (
- {({ change, data, hasChanged, submit }) => ( + {({ change, data, errors: formErrors, hasChanged, submit }) => ( {i18n.t("Product Types")} @@ -69,8 +85,24 @@ const ProductTypeCreatePage: React.StatelessComponent< + + + handleTaxTypeChange( + event, + taxTypes, + change, + setTaxTypeDisplayName + ) + } + />
- -
{ name: string; }; disabled: boolean; + errors: FormErrors<"name">; onChange: (event: React.ChangeEvent) => void; } const ProductTypeDetails = withStyles(styles, { name: "ProductTypeDetails" })( - ({ classes, data, disabled, onChange }: ProductTypeDetailsProps) => ( + ({ classes, data, disabled, errors, onChange }: ProductTypeDetailsProps) => ( ; + errors: UserError[]; productType: ProductTypeDetails_productType; defaultWeightUnit: WeightUnitsEnum; disabled: boolean; pageTitle: string; + productAttributeList: ListActions; saveButtonBarState: ConfirmButtonTransitionState; + taxTypes: ProductTypeDetails_taxTypes[]; + variantAttributeList: ListActions; onAttributeAdd: (type: AttributeTypeEnum) => void; - onAttributeDelete: (id: string, event: React.MouseEvent) => void; - onAttributeUpdate: (id: string) => void; + onAttributeClick: (id: string) => void; + onAttributeReorder: (event: ReorderEvent, type: AttributeTypeEnum) => void; + onAttributeUnassign: (id: string) => void; onBack: () => void; onDelete: () => void; onSubmit: (data: ProductTypeForm) => void; } +function handleTaxTypeChange( + event: ChangeEvent, + taxTypes: ProductTypeDetails_taxTypes[], + formChange: FormChange, + displayChange: (name: string) => void +) { + formChange(event); + displayChange( + taxTypes.find(taxType => taxType.taxCode === event.target.value).description + ); +} + const ProductTypeDetailsPage: React.StatelessComponent< ProductTypeDetailsPageProps > = ({ @@ -63,14 +78,21 @@ const ProductTypeDetailsPage: React.StatelessComponent< errors, pageTitle, productType, + productAttributeList, saveButtonBarState, + taxTypes, + variantAttributeList, onAttributeAdd, - onAttributeDelete, - onAttributeUpdate, + onAttributeUnassign, + onAttributeReorder, + onAttributeClick, onBack, onDelete, onSubmit }) => { + const [taxTypeDisplayName, setTaxTypeDisplayName] = useStateFromProps( + maybe(() => productType.taxType.description) + ); const formInitialData: ProductTypeForm = { hasVariants: maybe(() => productType.hasVariants) !== undefined @@ -88,10 +110,7 @@ const ProductTypeDetailsPage: React.StatelessComponent< value: attribute.id })) : [], - taxRate: - maybe(() => productType.taxRate) !== undefined - ? productType.taxRate - : null, + taxType: maybe(() => productType.taxType.taxCode, ""), variantAttributes: maybe(() => productType.variantAttributes) !== undefined ? productType.variantAttributes.map(attribute => ({ @@ -108,7 +127,7 @@ const ProductTypeDetailsPage: React.StatelessComponent< onSubmit={onSubmit} confirmLeave > - {({ change, data, hasChanged, submit }) => ( + {({ change, data, errors: formErrors, hasChanged, submit }) => ( {i18n.t("Product Types")} @@ -117,15 +136,36 @@ const ProductTypeDetailsPage: React.StatelessComponent< + + handleTaxTypeChange( + event, + taxTypes, + change, + setTaxTypeDisplayName + ) + } + /> + productType.productAttributes)} + disabled={disabled} type={AttributeTypeEnum.PRODUCT} - onAttributeAdd={onAttributeAdd} - onAttributeDelete={onAttributeDelete} - onAttributeUpdate={onAttributeUpdate} + onAttributeAssign={onAttributeAdd} + onAttributeClick={onAttributeClick} + onAttributeReorder={(event: ReorderEvent) => + onAttributeReorder(event, AttributeTypeEnum.PRODUCT) + } + onAttributeUnassign={onAttributeUnassign} + {...productAttributeList} /> productType.variantAttributes)} + disabled={disabled} type={AttributeTypeEnum.VARIANT} - onAttributeAdd={onAttributeAdd} - onAttributeDelete={onAttributeDelete} - onAttributeUpdate={onAttributeUpdate} + onAttributeAssign={onAttributeAdd} + onAttributeClick={onAttributeClick} + onAttributeReorder={(event: ReorderEvent) => + onAttributeReorder(event, AttributeTypeEnum.VARIANT) + } + onAttributeUnassign={onAttributeUnassign} + {...variantAttributeList} /> )} @@ -155,12 +200,6 @@ const ProductTypeDetailsPage: React.StatelessComponent< defaultWeightUnit={defaultWeightUnit} onChange={change} /> - -
toggle(productType.id)} /> @@ -148,8 +152,8 @@ const ProductTypeList = withStyles(styles, { name: "ProductTypeList" })( )} - {maybe(() => productType.taxRate) ? ( - translatedTaxRates()[productType.taxRate] + {maybe(() => productType.taxType) ? ( + productType.taxType.description ) : ( )} @@ -159,7 +163,7 @@ const ProductTypeList = withStyles(styles, { name: "ProductTypeList" })( }, () => ( - + {i18n.t("No product types found")} diff --git a/src/productTypes/components/ProductTypeTaxes/ProductTypeTaxes.tsx b/src/productTypes/components/ProductTypeTaxes/ProductTypeTaxes.tsx index 77dff8964..a03edfd79 100644 --- a/src/productTypes/components/ProductTypeTaxes/ProductTypeTaxes.tsx +++ b/src/productTypes/components/ProductTypeTaxes/ProductTypeTaxes.tsx @@ -1,44 +1,62 @@ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; +import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles"; import React from "react"; import CardTitle from "@saleor/components/CardTitle"; -import SingleSelectField from "@saleor/components/SingleSelectField"; +import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField"; +import { ProductTypeDetails_taxTypes } from "@saleor/productTypes/types/ProductTypeDetails"; import i18n from "../../../i18n"; -import { translatedTaxRates as taxRates } from "../../../misc"; -import { TaxRateType } from "../../../types/globalTypes"; +import { maybe } from "../../../misc"; +import { ProductTypeForm } from "../ProductTypeDetailsPage/ProductTypeDetailsPage"; -interface ProductTypeTaxesProps { +interface ProductTypeTaxesProps extends WithStyles { data: { - taxRate: TaxRateType | null; + taxType: string; }; + taxTypeDisplayName: string; + taxTypes: ProductTypeDetails_taxTypes[]; disabled: boolean; onChange: (event: React.ChangeEvent) => void; } -const taxRateChoices = Object.keys(taxRates()).map(key => ({ - label: taxRates()[key], - value: key -})); -const ProductTypeTaxes: React.StatelessComponent = ({ - data, - disabled, - onChange -}) => ( - - - - - - + +const styles = createStyles({ + root: { + overflow: "visible" + } +}); + +const ProductTypeTaxes = withStyles(styles, { name: "ProductTypeTaxes" })( + ({ + classes, + data, + disabled, + taxTypes, + taxTypeDisplayName, + onChange + }: ProductTypeTaxesProps) => ( + + + + + taxTypes.map(c => ({ label: c.description, value: c.taxCode })), + [] + )} + InputProps={{ + autoComplete: "off" + }} + /> + + + ) ); ProductTypeTaxes.displayName = "ProductTypeTaxes"; export default ProductTypeTaxes; diff --git a/src/productTypes/containers/ProductTypeOperations.tsx b/src/productTypes/containers/ProductTypeOperations.tsx index ce34e2660..7a7ea2931 100644 --- a/src/productTypes/containers/ProductTypeOperations.tsx +++ b/src/productTypes/containers/ProductTypeOperations.tsx @@ -1,63 +1,91 @@ import React from "react"; +import { MutationFn } from "react-apollo"; +import { AttributeTypeEnum, ReorderInput } from "@saleor/types/globalTypes"; import { getMutationProviderData } from "../../misc"; import { PartialMutationProviderOutput } from "../../types"; import { - TypedAttributeCreateMutation, - TypedAttributeDeleteMutation, - TypedAttributeUpdateMutation, + ProductTypeAttributeReorderMutation, + TypedAssignAttributeMutation, TypedProductTypeDeleteMutation, - TypedProductTypeUpdateMutation + TypedProductTypeUpdateMutation, + TypedUnassignAttributeMutation } from "../mutations"; import { - AttributeCreate, - AttributeCreateVariables -} from "../types/AttributeCreate"; + AssignAttribute, + AssignAttributeVariables +} from "../types/AssignAttribute"; import { - AttributeDelete, - AttributeDeleteVariables -} from "../types/AttributeDelete"; -import { - AttributeUpdate, - AttributeUpdateVariables -} from "../types/AttributeUpdate"; + ProductTypeAttributeReorder, + ProductTypeAttributeReorderVariables +} from "../types/ProductTypeAttributeReorder"; import { ProductTypeDelete, ProductTypeDeleteVariables } from "../types/ProductTypeDelete"; +import { + ProductTypeDetailsFragment, + ProductTypeDetailsFragment_productAttributes +} from "../types/ProductTypeDetailsFragment"; import { ProductTypeUpdate, ProductTypeUpdateVariables } from "../types/ProductTypeUpdate"; +import { + UnassignAttribute, + UnassignAttributeVariables +} from "../types/UnassignAttribute"; + +function moveAttribute( + attributes: + | ProductTypeDetailsFragment_productAttributes[] + | ProductTypeDetailsFragment_productAttributes[], + move: ReorderInput +) { + const attributeIndex = attributes.findIndex( + attribute => attribute.id === move.id + ); + const newIndex = attributeIndex + move.sortOrder; + + const attributesWithoutMovedOne = [ + ...attributes.slice(0, attributeIndex), + ...attributes.slice(attributeIndex + 1) + ]; + + return [ + ...attributesWithoutMovedOne.slice(0, newIndex), + attributes[attributeIndex], + ...attributesWithoutMovedOne.slice(newIndex) + ]; +} interface ProductTypeOperationsProps { - children: ( - props: { - attributeCreate: PartialMutationProviderOutput< - AttributeCreate, - AttributeCreateVariables - >; - deleteAttribute: PartialMutationProviderOutput< - AttributeDelete, - AttributeDeleteVariables - >; - deleteProductType: PartialMutationProviderOutput< - ProductTypeDelete, - ProductTypeDeleteVariables - >; - updateAttribute: PartialMutationProviderOutput< - AttributeUpdate, - AttributeUpdateVariables - >; - updateProductType: PartialMutationProviderOutput< - ProductTypeUpdate, - ProductTypeUpdateVariables - >; - } - ) => React.ReactNode; - onAttributeCreate: (data: AttributeCreate) => void; - onAttributeDelete: (data: AttributeDelete) => void; - onAttributeUpdate: (data: AttributeUpdate) => void; + children: (props: { + assignAttribute: PartialMutationProviderOutput< + AssignAttribute, + AssignAttributeVariables + >; + unassignAttribute: PartialMutationProviderOutput< + UnassignAttribute, + UnassignAttributeVariables + >; + deleteProductType: PartialMutationProviderOutput< + ProductTypeDelete, + ProductTypeDeleteVariables + >; + reorderAttribute: PartialMutationProviderOutput< + ProductTypeAttributeReorder, + ProductTypeAttributeReorderVariables + >; + updateProductType: PartialMutationProviderOutput< + ProductTypeUpdate, + ProductTypeUpdateVariables + >; + }) => React.ReactNode; + productType: ProductTypeDetailsFragment; + onAssignAttribute: (data: AssignAttribute) => void; + onUnassignAttribute: (data: UnassignAttribute) => void; + onProductTypeAttributeReorder: (data: ProductTypeAttributeReorder) => void; onProductTypeDelete: (data: ProductTypeDelete) => void; onProductTypeUpdate: (data: ProductTypeUpdate) => void; } @@ -66,9 +94,10 @@ const ProductTypeOperations: React.StatelessComponent< ProductTypeOperationsProps > = ({ children, - onAttributeCreate, - onAttributeDelete, - onAttributeUpdate, + productType, + onAssignAttribute, + onUnassignAttribute, + onProductTypeAttributeReorder, onProductTypeDelete, onProductTypeUpdate }) => { @@ -77,37 +106,78 @@ const ProductTypeOperations: React.StatelessComponent< {(...deleteProductType) => ( {(...updateProductType) => ( - - {(...createAttribute) => ( - - {(...deleteAttribute) => ( - + {(...assignAttribute) => ( + + {(...unassignAttribute) => ( + - {(...updateAttribute) => - children({ - attributeCreate: getMutationProviderData( - ...createAttribute - ), - deleteAttribute: getMutationProviderData( - ...deleteAttribute + {( + reorderAttributeMutation, + reorderAttributeMutationResult + ) => { + const reorderAttributeMutationFn: MutationFn< + ProductTypeAttributeReorder, + ProductTypeAttributeReorderVariables + > = opts => { + const optimisticResponse: ProductTypeAttributeReorder = { + productTypeReorderAttributes: { + __typename: "ProductTypeReorderAttributes" as "ProductTypeReorderAttributes", + errors: [], + productType: { + ...productType, + productAttributes: + opts.variables.type === + AttributeTypeEnum.PRODUCT + ? moveAttribute( + productType.productAttributes, + opts.variables.move + ) + : productType.productAttributes, + variantAttributes: + opts.variables.type === + AttributeTypeEnum.VARIANT + ? moveAttribute( + productType.variantAttributes, + opts.variables.move + ) + : productType.variantAttributes + } + } + }; + return reorderAttributeMutation({ + ...opts, + optimisticResponse + }); + }; + + return children({ + assignAttribute: getMutationProviderData( + ...assignAttribute ), deleteProductType: getMutationProviderData( ...deleteProductType ), - updateAttribute: getMutationProviderData( - ...updateAttribute + reorderAttribute: getMutationProviderData( + reorderAttributeMutationFn, + reorderAttributeMutationResult + ), + unassignAttribute: getMutationProviderData( + ...unassignAttribute ), updateProductType: getMutationProviderData( ...updateProductType ) - }) - } - + }); + }} + )} - + )} - + )} )} diff --git a/src/productTypes/fixtures.ts b/src/productTypes/fixtures.ts index 169f08b8f..fe214f7bd 100644 --- a/src/productTypes/fixtures.ts +++ b/src/productTypes/fixtures.ts @@ -2,7 +2,7 @@ import { ProductCreateData_productTypes_edges_node, ProductCreateData_productTypes_edges_node_productAttributes } from "../products/types/ProductCreateData"; -import { TaxRateType } from "../types/globalTypes"; +import { AttributeInputTypeEnum } from "../types/globalTypes"; import { ProductTypeDetails_productType } from "./types/ProductTypeDetails"; import { ProductTypeList_productTypes_edges_node } from "./types/ProductTypeList"; @@ -11,8 +11,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZTo5", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Author", slug: "author", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -39,8 +41,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZTo2", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Box Size", slug: "box-size", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -85,8 +89,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZToz", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Brand", slug: "brand", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -104,8 +110,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZTo4", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Candy Box Size", slug: "candy-box-size", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -141,8 +149,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZTo1", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Coffee Genre", slug: "coffee-genre", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -169,8 +179,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZToy", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Collar", slug: "collar", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -206,8 +218,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZTox", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Color", slug: "color", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -234,8 +248,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZToxMg==", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Cover", slug: "cover", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -298,8 +314,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZTo3", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Flavor", slug: "flavor", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -326,8 +344,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZToxMQ==", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Language", slug: "language", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -354,8 +374,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZToxMA==", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Publisher", slug: "publisher", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -382,8 +404,10 @@ export const attributes: ProductCreateData_productTypes_edges_node_productAttrib node: { __typename: "Attribute" as "Attribute", id: "UHJvZHVjdEF0dHJpYnV0ZTo0", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Size", slug: "size", + valueRequired: true, values: [ { __typename: "AttributeValue" as "AttributeValue", @@ -455,7 +479,11 @@ export const productTypes: Array< isShippingRequired: true, name: "Candy", productAttributes: [attributes[0]], - taxRate: "FOODSTUFFS" as TaxRateType, + taxType: { + __typename: "TaxType" as "TaxType", + description: "PB100000", + taxCode: "Books / Manuals" + }, variantAttributes: [attributes[1], attributes[2]] }, { @@ -465,7 +493,11 @@ export const productTypes: Array< isShippingRequired: false, name: "E-books", productAttributes: [attributes[5]], - taxRate: "STANDARD" as TaxRateType, + taxType: { + __typename: "TaxType" as "TaxType", + description: "PH403682", + taxCode: "Holters" + }, variantAttributes: [attributes[0], attributes[3]] }, { @@ -475,7 +507,11 @@ export const productTypes: Array< isShippingRequired: true, name: "Mugs", productAttributes: [attributes[7]], - taxRate: "STANDARD" as TaxRateType, + taxType: { + __typename: "TaxType" as "TaxType", + description: "PC077426", + taxCode: "Cabling" + }, variantAttributes: [attributes[2], attributes[5]] }, { @@ -485,7 +521,11 @@ export const productTypes: Array< isShippingRequired: true, name: "Coffee", productAttributes: [attributes[8]], - taxRate: "STANDARD" as TaxRateType, + taxType: { + __typename: "TaxType" as "TaxType", + description: "PB100000", + taxCode: "Books / Manuals" + }, variantAttributes: [attributes[1], attributes[4]] }, { @@ -495,7 +535,11 @@ export const productTypes: Array< isShippingRequired: true, name: "T-Shirt", productAttributes: [attributes[4]], - taxRate: "STANDARD" as TaxRateType, + taxType: { + __typename: "TaxType" as "TaxType", + description: "PH403970", + taxCode: "Wheelchair" + }, variantAttributes: [attributes[1], attributes[6]] } ].map(productType => ({ @@ -511,67 +555,51 @@ export const productType: ProductTypeDetails_productType = { productAttributes: [ { __typename: "Attribute" as "Attribute", + filterableInDashboard: true, + filterableInStorefront: false, id: "UHJvZHVjdEF0dHJpYnV0ZTo5", name: "Author", slug: "author", - values: [ - { - __typename: "AttributeValue" as "AttributeValue", - id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI0", - name: "John Doe", - slug: "john-doe" - }, - { - __typename: "AttributeValue" as "AttributeValue", - id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI1", - name: "Milionare Pirate", - slug: "milionare-pirate" - } - ] + visibleInStorefront: true }, { __typename: "Attribute" as "Attribute", + filterableInDashboard: true, + filterableInStorefront: false, id: "UHJvZHVjdEF0dHJpYnV0ZToxMQ==", name: "Language", slug: "language", - values: [ - { - __typename: "AttributeValue" as "AttributeValue", - id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI4", - name: "English", - slug: "english" - }, - { - __typename: "AttributeValue" as "AttributeValue", - id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI5", - name: "Pirate", - slug: "pirate" - } - ] + visibleInStorefront: true }, { __typename: "Attribute" as "Attribute", + filterableInDashboard: true, + filterableInStorefront: false, id: "UHJvZHVjdEF0dHJpYnV0ZToxMA==", name: "Publisher", slug: "publisher", - values: [ - { - __typename: "AttributeValue" as "AttributeValue", - id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI2", - name: "Mirumee Press", - slug: "mirumee-press" - }, - { - __typename: "AttributeValue" as "AttributeValue", - id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI3", - name: "Saleor Publishing", - slug: "saleor-publishing" - } - ] + visibleInStorefront: true + } + ], + taxType: { + __typename: "TaxType" as "TaxType", + description: "PH405458", + taxCode: "Shields" + }, + variantAttributes: [ + { + ...attributes[1], + filterableInDashboard: true, + filterableInStorefront: false, + visibleInStorefront: true + }, + { + ...attributes[6], + filterableInDashboard: true, + filterableInStorefront: false, + visibleInStorefront: true } ], - taxRate: "STANDARD" as TaxRateType, - variantAttributes: [], weight: { __typename: "Weight", unit: "kg", diff --git a/src/productTypes/mutations.ts b/src/productTypes/mutations.ts index de7cc9543..7d103d756 100644 --- a/src/productTypes/mutations.ts +++ b/src/productTypes/mutations.ts @@ -1,19 +1,15 @@ import gql from "graphql-tag"; import { TypedMutation } from "../mutations"; -import { attributeFragment, productTypeDetailsFragment } from "./queries"; +import { productTypeDetailsFragment } from "./queries"; import { - AttributeCreate, - AttributeCreateVariables -} from "./types/AttributeCreate"; + AssignAttribute, + AssignAttributeVariables +} from "./types/AssignAttribute"; import { - AttributeDelete, - AttributeDeleteVariables -} from "./types/AttributeDelete"; -import { - AttributeUpdate, - AttributeUpdateVariables -} from "./types/AttributeUpdate"; + ProductTypeAttributeReorder, + ProductTypeAttributeReorderVariables +} from "./types/ProductTypeAttributeReorder"; import { ProductTypeBulkDelete, ProductTypeBulkDeleteVariables @@ -30,6 +26,10 @@ import { ProductTypeUpdate, ProductTypeUpdateVariables } from "./types/ProductTypeUpdate"; +import { + UnassignAttribute, + UnassignAttributeVariables +} from "./types/UnassignAttribute"; export const productTypeDeleteMutation = gql` mutation ProductTypeDelete($id: ID!) { @@ -83,6 +83,44 @@ export const TypedProductTypeUpdateMutation = TypedMutation< ProductTypeUpdateVariables >(productTypeUpdateMutation); +export const assignAttributeMutation = gql` + ${productTypeDetailsFragment} + mutation AssignAttribute($id: ID!, $operations: [AttributeAssignInput!]!) { + attributeAssign(productTypeId: $id, operations: $operations) { + errors { + field + message + } + productType { + ...ProductTypeDetailsFragment + } + } + } +`; +export const TypedAssignAttributeMutation = TypedMutation< + AssignAttribute, + AssignAttributeVariables +>(assignAttributeMutation); + +export const unassignAttributeMutation = gql` + ${productTypeDetailsFragment} + mutation UnassignAttribute($id: ID!, $ids: [ID]!) { + attributeUnassign(productTypeId: $id, attributeIds: $ids) { + errors { + field + message + } + productType { + ...ProductTypeDetailsFragment + } + } + } +`; +export const TypedUnassignAttributeMutation = TypedMutation< + UnassignAttribute, + UnassignAttributeVariables +>(unassignAttributeMutation); + export const productTypeCreateMutation = gql` ${productTypeDetailsFragment} mutation ProductTypeCreate($input: ProductTypeInput!) { @@ -102,14 +140,18 @@ export const TypedProductTypeCreateMutation = TypedMutation< ProductTypeCreateVariables >(productTypeCreateMutation); -export const attributeCreateMutation = gql` +const productTypeAttributeReorder = gql` ${productTypeDetailsFragment} - mutation AttributeCreate( - $id: ID! - $input: AttributeCreateInput! + mutation ProductTypeAttributeReorder( + $move: ReorderInput! + $productTypeId: ID! $type: AttributeTypeEnum! ) { - attributeCreate(id: $id, input: $input, type: $type) { + productTypeReorderAttributes( + moves: [$move] + productTypeId: $productTypeId + type: $type + ) { errors { field message @@ -120,45 +162,7 @@ export const attributeCreateMutation = gql` } } `; -export const TypedAttributeCreateMutation = TypedMutation< - AttributeCreate, - AttributeCreateVariables ->(attributeCreateMutation); - -export const attributeUpdateMutation = gql` - ${attributeFragment} - mutation AttributeUpdate($id: ID!, $input: AttributeUpdateInput!) { - attributeUpdate(id: $id, input: $input) { - errors { - field - message - } - attribute { - ...AttributeFragment - } - } - } -`; -export const TypedAttributeUpdateMutation = TypedMutation< - AttributeUpdate, - AttributeUpdateVariables ->(attributeUpdateMutation); - -export const attributeDeleteMutation = gql` - ${productTypeDetailsFragment} - mutation AttributeDelete($id: ID!) { - attributeDelete(id: $id) { - errors { - field - message - } - productType { - ...ProductTypeDetailsFragment - } - } - } -`; -export const TypedAttributeDeleteMutation = TypedMutation< - AttributeDelete, - AttributeDeleteVariables ->(attributeDeleteMutation); +export const ProductTypeAttributeReorderMutation = TypedMutation< + ProductTypeAttributeReorder, + ProductTypeAttributeReorderVariables +>(productTypeAttributeReorder); diff --git a/src/productTypes/queries.ts b/src/productTypes/queries.ts index f3012c73a..63bbbbec0 100644 --- a/src/productTypes/queries.ts +++ b/src/productTypes/queries.ts @@ -1,6 +1,7 @@ import gql from "graphql-tag"; -import { TypedQuery } from "../queries"; +import { attributeFragment } from "@saleor/attributes/queries"; +import { pageInfoFragment, TypedQuery } from "../queries"; import { ProductTypeCreateData } from "./types/ProductTypeCreateData"; import { ProductTypeDetails, @@ -11,25 +12,16 @@ import { ProductTypeListVariables } from "./types/ProductTypeList"; -export const attributeFragment = gql` - fragment AttributeFragment on Attribute { - id - name - slug - values { - id - name - slug - } - } -`; export const productTypeFragment = gql` fragment ProductTypeFragment on ProductType { id name hasVariants isShippingRequired - taxRate + taxType { + description + taxCode + } } `; @@ -52,6 +44,7 @@ export const productTypeDetailsFragment = gql` `; export const productTypeListQuery = gql` + ${pageInfoFragment} ${productTypeFragment} query ProductTypeList( $after: String @@ -66,10 +59,7 @@ export const productTypeListQuery = gql` } } pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor + ...PageInfoFragment } } } @@ -88,6 +78,10 @@ export const productTypeDetailsQuery = gql` shop { defaultWeightUnit } + taxTypes { + taxCode + description + } } `; export const TypedProductTypeDetailsQuery = TypedQuery< @@ -100,6 +94,10 @@ export const productTypeCreateDataQuery = gql` shop { defaultWeightUnit } + taxTypes { + taxCode + description + } } `; export const TypedProductTypeCreateDataQuery = TypedQuery< diff --git a/src/productTypes/types/ProductTypeCreate.ts b/src/productTypes/types/ProductTypeCreate.ts index a146ea60f..b2c9256c8 100644 --- a/src/productTypes/types/ProductTypeCreate.ts +++ b/src/productTypes/types/ProductTypeCreate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { ProductTypeInput, TaxRateType } from "./../../types/globalTypes"; +import { ProductTypeInput } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: ProductTypeCreate @@ -14,11 +14,10 @@ export interface ProductTypeCreate_productTypeCreate_errors { message: string | null; } -export interface ProductTypeCreate_productTypeCreate_productType_productAttributes_values { - __typename: "AttributeValue"; - id: string; - name: string | null; - slug: string | null; +export interface ProductTypeCreate_productTypeCreate_productType_taxType { + __typename: "TaxType"; + description: string | null; + taxCode: string | null; } export interface ProductTypeCreate_productTypeCreate_productType_productAttributes { @@ -26,14 +25,9 @@ export interface ProductTypeCreate_productTypeCreate_productType_productAttribut id: string; name: string | null; slug: string | null; - values: (ProductTypeCreate_productTypeCreate_productType_productAttributes_values | null)[] | null; -} - -export interface ProductTypeCreate_productTypeCreate_productType_variantAttributes_values { - __typename: "AttributeValue"; - id: string; - name: string | null; - slug: string | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; } export interface ProductTypeCreate_productTypeCreate_productType_variantAttributes { @@ -41,7 +35,9 @@ export interface ProductTypeCreate_productTypeCreate_productType_variantAttribut id: string; name: string | null; slug: string | null; - values: (ProductTypeCreate_productTypeCreate_productType_variantAttributes_values | null)[] | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; } export interface ProductTypeCreate_productTypeCreate_productType_weight { @@ -56,7 +52,7 @@ export interface ProductTypeCreate_productTypeCreate_productType { name: string; hasVariants: boolean; isShippingRequired: boolean; - taxRate: TaxRateType | null; + taxType: ProductTypeCreate_productTypeCreate_productType_taxType | null; productAttributes: (ProductTypeCreate_productTypeCreate_productType_productAttributes | null)[] | null; variantAttributes: (ProductTypeCreate_productTypeCreate_productType_variantAttributes | null)[] | null; weight: ProductTypeCreate_productTypeCreate_productType_weight | null; diff --git a/src/productTypes/types/ProductTypeCreateData.ts b/src/productTypes/types/ProductTypeCreateData.ts index 483dedd6a..b50c7f29c 100644 --- a/src/productTypes/types/ProductTypeCreateData.ts +++ b/src/productTypes/types/ProductTypeCreateData.ts @@ -13,6 +13,13 @@ export interface ProductTypeCreateData_shop { defaultWeightUnit: WeightUnitsEnum | null; } +export interface ProductTypeCreateData_taxTypes { + __typename: "TaxType"; + taxCode: string | null; + description: string | null; +} + export interface ProductTypeCreateData { shop: ProductTypeCreateData_shop | null; + taxTypes: (ProductTypeCreateData_taxTypes | null)[] | null; } diff --git a/src/productTypes/types/ProductTypeDetails.ts b/src/productTypes/types/ProductTypeDetails.ts index 38cbc73a6..ec3d8a568 100644 --- a/src/productTypes/types/ProductTypeDetails.ts +++ b/src/productTypes/types/ProductTypeDetails.ts @@ -2,17 +2,16 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { TaxRateType, WeightUnitsEnum } from "./../../types/globalTypes"; +import { WeightUnitsEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL query operation: ProductTypeDetails // ==================================================== -export interface ProductTypeDetails_productType_productAttributes_values { - __typename: "AttributeValue"; - id: string; - name: string | null; - slug: string | null; +export interface ProductTypeDetails_productType_taxType { + __typename: "TaxType"; + description: string | null; + taxCode: string | null; } export interface ProductTypeDetails_productType_productAttributes { @@ -20,14 +19,9 @@ export interface ProductTypeDetails_productType_productAttributes { id: string; name: string | null; slug: string | null; - values: (ProductTypeDetails_productType_productAttributes_values | null)[] | null; -} - -export interface ProductTypeDetails_productType_variantAttributes_values { - __typename: "AttributeValue"; - id: string; - name: string | null; - slug: string | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; } export interface ProductTypeDetails_productType_variantAttributes { @@ -35,7 +29,9 @@ export interface ProductTypeDetails_productType_variantAttributes { id: string; name: string | null; slug: string | null; - values: (ProductTypeDetails_productType_variantAttributes_values | null)[] | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; } export interface ProductTypeDetails_productType_weight { @@ -50,7 +46,7 @@ export interface ProductTypeDetails_productType { name: string; hasVariants: boolean; isShippingRequired: boolean; - taxRate: TaxRateType | null; + taxType: ProductTypeDetails_productType_taxType | null; productAttributes: (ProductTypeDetails_productType_productAttributes | null)[] | null; variantAttributes: (ProductTypeDetails_productType_variantAttributes | null)[] | null; weight: ProductTypeDetails_productType_weight | null; @@ -61,9 +57,16 @@ export interface ProductTypeDetails_shop { defaultWeightUnit: WeightUnitsEnum | null; } +export interface ProductTypeDetails_taxTypes { + __typename: "TaxType"; + taxCode: string | null; + description: string | null; +} + export interface ProductTypeDetails { productType: ProductTypeDetails_productType | null; shop: ProductTypeDetails_shop | null; + taxTypes: (ProductTypeDetails_taxTypes | null)[] | null; } export interface ProductTypeDetailsVariables { diff --git a/src/productTypes/types/ProductTypeDetailsFragment.ts b/src/productTypes/types/ProductTypeDetailsFragment.ts index 85b673786..87cd9d547 100644 --- a/src/productTypes/types/ProductTypeDetailsFragment.ts +++ b/src/productTypes/types/ProductTypeDetailsFragment.ts @@ -2,17 +2,14 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { TaxRateType } from "./../../types/globalTypes"; - // ==================================================== // GraphQL fragment: ProductTypeDetailsFragment // ==================================================== -export interface ProductTypeDetailsFragment_productAttributes_values { - __typename: "AttributeValue"; - id: string; - name: string | null; - slug: string | null; +export interface ProductTypeDetailsFragment_taxType { + __typename: "TaxType"; + description: string | null; + taxCode: string | null; } export interface ProductTypeDetailsFragment_productAttributes { @@ -20,14 +17,9 @@ export interface ProductTypeDetailsFragment_productAttributes { id: string; name: string | null; slug: string | null; - values: (ProductTypeDetailsFragment_productAttributes_values | null)[] | null; -} - -export interface ProductTypeDetailsFragment_variantAttributes_values { - __typename: "AttributeValue"; - id: string; - name: string | null; - slug: string | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; } export interface ProductTypeDetailsFragment_variantAttributes { @@ -35,7 +27,9 @@ export interface ProductTypeDetailsFragment_variantAttributes { id: string; name: string | null; slug: string | null; - values: (ProductTypeDetailsFragment_variantAttributes_values | null)[] | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; } export interface ProductTypeDetailsFragment_weight { @@ -50,7 +44,7 @@ export interface ProductTypeDetailsFragment { name: string; hasVariants: boolean; isShippingRequired: boolean; - taxRate: TaxRateType | null; + taxType: ProductTypeDetailsFragment_taxType | null; productAttributes: (ProductTypeDetailsFragment_productAttributes | null)[] | null; variantAttributes: (ProductTypeDetailsFragment_variantAttributes | null)[] | null; weight: ProductTypeDetailsFragment_weight | null; diff --git a/src/productTypes/types/ProductTypeFragment.ts b/src/productTypes/types/ProductTypeFragment.ts index f1443f1ff..d96c18668 100644 --- a/src/productTypes/types/ProductTypeFragment.ts +++ b/src/productTypes/types/ProductTypeFragment.ts @@ -2,17 +2,21 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { TaxRateType } from "./../../types/globalTypes"; - // ==================================================== // GraphQL fragment: ProductTypeFragment // ==================================================== +export interface ProductTypeFragment_taxType { + __typename: "TaxType"; + description: string | null; + taxCode: string | null; +} + export interface ProductTypeFragment { __typename: "ProductType"; id: string; name: string; hasVariants: boolean; isShippingRequired: boolean; - taxRate: TaxRateType | null; + taxType: ProductTypeFragment_taxType | null; } diff --git a/src/productTypes/types/ProductTypeList.ts b/src/productTypes/types/ProductTypeList.ts index 89d1f26d0..64fa7e917 100644 --- a/src/productTypes/types/ProductTypeList.ts +++ b/src/productTypes/types/ProductTypeList.ts @@ -2,19 +2,23 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { TaxRateType } from "./../../types/globalTypes"; - // ==================================================== // GraphQL query operation: ProductTypeList // ==================================================== +export interface ProductTypeList_productTypes_edges_node_taxType { + __typename: "TaxType"; + description: string | null; + taxCode: string | null; +} + export interface ProductTypeList_productTypes_edges_node { __typename: "ProductType"; id: string; name: string; hasVariants: boolean; isShippingRequired: boolean; - taxRate: TaxRateType | null; + taxType: ProductTypeList_productTypes_edges_node_taxType | null; } export interface ProductTypeList_productTypes_edges { @@ -24,10 +28,10 @@ export interface ProductTypeList_productTypes_edges { export interface ProductTypeList_productTypes_pageInfo { __typename: "PageInfo"; + endCursor: string | null; hasNextPage: boolean; hasPreviousPage: boolean; startCursor: string | null; - endCursor: string | null; } export interface ProductTypeList_productTypes { diff --git a/src/productTypes/types/ProductTypeUpdate.ts b/src/productTypes/types/ProductTypeUpdate.ts index 6c7f4ce11..b0859f729 100644 --- a/src/productTypes/types/ProductTypeUpdate.ts +++ b/src/productTypes/types/ProductTypeUpdate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { ProductTypeInput, TaxRateType } from "./../../types/globalTypes"; +import { ProductTypeInput } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: ProductTypeUpdate @@ -14,11 +14,10 @@ export interface ProductTypeUpdate_productTypeUpdate_errors { message: string | null; } -export interface ProductTypeUpdate_productTypeUpdate_productType_productAttributes_values { - __typename: "AttributeValue"; - id: string; - name: string | null; - slug: string | null; +export interface ProductTypeUpdate_productTypeUpdate_productType_taxType { + __typename: "TaxType"; + description: string | null; + taxCode: string | null; } export interface ProductTypeUpdate_productTypeUpdate_productType_productAttributes { @@ -26,14 +25,9 @@ export interface ProductTypeUpdate_productTypeUpdate_productType_productAttribut id: string; name: string | null; slug: string | null; - values: (ProductTypeUpdate_productTypeUpdate_productType_productAttributes_values | null)[] | null; -} - -export interface ProductTypeUpdate_productTypeUpdate_productType_variantAttributes_values { - __typename: "AttributeValue"; - id: string; - name: string | null; - slug: string | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; } export interface ProductTypeUpdate_productTypeUpdate_productType_variantAttributes { @@ -41,7 +35,9 @@ export interface ProductTypeUpdate_productTypeUpdate_productType_variantAttribut id: string; name: string | null; slug: string | null; - values: (ProductTypeUpdate_productTypeUpdate_productType_variantAttributes_values | null)[] | null; + visibleInStorefront: boolean; + filterableInDashboard: boolean; + filterableInStorefront: boolean; } export interface ProductTypeUpdate_productTypeUpdate_productType_weight { @@ -56,7 +52,7 @@ export interface ProductTypeUpdate_productTypeUpdate_productType { name: string; hasVariants: boolean; isShippingRequired: boolean; - taxRate: TaxRateType | null; + taxType: ProductTypeUpdate_productTypeUpdate_productType_taxType | null; productAttributes: (ProductTypeUpdate_productTypeUpdate_productType_productAttributes | null)[] | null; variantAttributes: (ProductTypeUpdate_productTypeUpdate_productType_variantAttributes | null)[] | null; weight: ProductTypeUpdate_productTypeUpdate_productType_weight | null; diff --git a/src/productTypes/urls.ts b/src/productTypes/urls.ts index 2ba27d635..86251789d 100644 --- a/src/productTypes/urls.ts +++ b/src/productTypes/urls.ts @@ -2,7 +2,6 @@ import { stringify as stringifyQs } from "qs"; import urlJoin from "url-join"; import { BulkAction, Dialog, Pagination, SingleAction } from "../types"; -import { AttributeTypeEnum } from "../types/globalTypes"; const productTypeSection = "/product-types/"; @@ -19,12 +18,14 @@ export const productTypeAddUrl = productTypeAddPath; export const productTypePath = (id: string) => urlJoin(productTypeSection, id); export type ProductTypeUrlDialog = - | "add-attribute" - | "edit-attribute" + | "assign-attribute" + | "unassign-attribute" + | "unassign-attributes" | "remove"; -export type ProductTypeUrlQueryParams = Dialog & +export type ProductTypeUrlQueryParams = BulkAction & + Dialog & SingleAction & { - type?: AttributeTypeEnum; + type?: string; }; export const productTypeUrl = ( id: string, diff --git a/src/productTypes/views/ProductTypeCreate.tsx b/src/productTypes/views/ProductTypeCreate.tsx index 51bc8cab0..2427377f1 100644 --- a/src/productTypes/views/ProductTypeCreate.tsx +++ b/src/productTypes/views/ProductTypeCreate.tsx @@ -4,7 +4,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import i18n from "../../i18n"; -import { maybe } from "../../misc"; +import { getMutationState, maybe } from "../../misc"; import ProductTypeCreatePage, { ProductTypeForm } from "../components/ProductTypeCreatePage"; @@ -29,8 +29,9 @@ export const ProductTypeCreate: React.StatelessComponent = () => { {( createProductType, - { loading: loadingCreate, data: createProductTypeData } + { called, loading, data: createProductTypeData } ) => { + const formTransitionState = getMutationState(loading, called); const handleCreate = (formData: ProductTypeForm) => createProductType({ variables: { @@ -38,7 +39,7 @@ export const ProductTypeCreate: React.StatelessComponent = () => { hasVariants: false, isShippingRequired: formData.isShippingRequired, name: formData.name, - taxRate: formData.chargeTaxes ? formData.taxRate : null, + taxCode: formData.taxType, weight: formData.weight } } @@ -50,7 +51,7 @@ export const ProductTypeCreate: React.StatelessComponent = () => { data.shop.defaultWeightUnit)} - disabled={loadingCreate || loading} + disabled={loading} errors={ createProductTypeData ? createProductTypeData.productTypeCreate.errors @@ -59,7 +60,8 @@ export const ProductTypeCreate: React.StatelessComponent = () => { pageTitle={i18n.t("Create Product Type", { context: "page title" })} - saveButtonBarState={loadingCreate ? "loading" : "default"} + saveButtonBarState={formTransitionState} + taxTypes={maybe(() => data.taxTypes, [])} onBack={() => navigate(productTypeListUrl())} onSubmit={handleCreate} /> diff --git a/src/productTypes/views/ProductTypeUpdate/index.tsx b/src/productTypes/views/ProductTypeUpdate/index.tsx index 25348b6ec..1e59ccb7c 100644 --- a/src/productTypes/views/ProductTypeUpdate/index.tsx +++ b/src/productTypes/views/ProductTypeUpdate/index.tsx @@ -1,26 +1,29 @@ -import DialogContentText from "@material-ui/core/DialogContentText"; +import Button from "@material-ui/core/Button"; import React from "react"; -import ActionDialog from "@saleor/components/ActionDialog"; +import { attributeUrl } from "@saleor/attributes/urls"; import { WindowTitle } from "@saleor/components/WindowTitle"; +import useBulkActions from "@saleor/hooks/useBulkActions"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; -import i18n from "../../../i18n"; -import { getMutationState, maybe } from "../../../misc"; -import { AttributeTypeEnum } from "../../../types/globalTypes"; -import ProductTypeAttributeEditDialog, { - FormData as AttributeForm -} from "../../components/ProductTypeAttributeEditDialog"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import AssignAttributeDialog from "@saleor/productTypes/components/AssignAttributeDialog"; +import { ReorderEvent } from "@saleor/types"; +import { AttributeTypeEnum } from "@saleor/types/globalTypes"; +import ProductTypeAttributeUnassignDialog from "../../components/ProductTypeAttributeUnassignDialog"; +import ProductTypeBulkAttributeUnassignDialog from "../../components/ProductTypeBulkAttributeUnassignDialog"; +import ProductTypeDeleteDialog from "../../components/ProductTypeDeleteDialog"; import ProductTypeDetailsPage, { ProductTypeForm } from "../../components/ProductTypeDetailsPage"; import ProductTypeOperations from "../../containers/ProductTypeOperations"; +import SearchAttributes from "../../containers/SearchAttributes"; import { TypedProductTypeDetailsQuery } from "../../queries"; -import { AttributeCreate } from "../../types/AttributeCreate"; -import { AttributeDelete } from "../../types/AttributeDelete"; -import { AttributeUpdate } from "../../types/AttributeUpdate"; +import { AssignAttribute } from "../../types/AssignAttribute"; import { ProductTypeDelete } from "../../types/ProductTypeDelete"; import { ProductTypeUpdate as ProductTypeUpdateMutation } from "../../types/ProductTypeUpdate"; +import { UnassignAttribute } from "../../types/UnassignAttribute"; import { productTypeListUrl, productTypeUrl, @@ -33,11 +36,14 @@ interface ProductTypeUpdateProps { params: ProductTypeUrlQueryParams; } -export const ProductTypeUpdate: React.StatelessComponent< - ProductTypeUpdateProps -> = ({ id, params }) => { +export const ProductTypeUpdate: React.FC = ({ + id, + params +}) => { const navigate = useNavigator(); const notify = useNotifier(); + const productAttributeListActions = useBulkActions(); + const variantAttributeListActions = useBulkActions(); return ( @@ -48,48 +54,35 @@ export const ProductTypeUpdate: React.StatelessComponent< require={["productType"]} > {({ data, loading: dataLoading }) => { - const closeModal = () => { - navigate(productTypeUrl(id), true); - setErrors.addAttributeErrors([]); - setErrors.editAttributeErrors([]); - }; - const handleAttributeCreateSuccess = (data: AttributeCreate) => { - if (data.attributeCreate.errors.length === 0) { + const closeModal = () => navigate(productTypeUrl(id), true); + + const handleAttributeAssignSuccess = (data: AssignAttribute) => { + if (data.attributeAssign.errors.length === 0) { notify({ - text: i18n.t("Attribute created", { + text: i18n.t("Attributes assigned", { context: "notification" }) }); closeModal(); } else if ( - data.attributeCreate.errors !== null && - data.attributeCreate.errors.length > 0 + data.attributeAssign.errors !== null && + data.attributeAssign.errors.length > 0 ) { - setErrors.addAttributeErrors(data.attributeCreate.errors); + setErrors.addAttributeErrors(data.attributeAssign.errors); } }; - const handleAttributeDeleteSuccess = (data: AttributeDelete) => { - if (!data.attributeDelete.errors) { + const handleAttributeUnassignSuccess = ( + data: UnassignAttribute + ) => { + if (data.attributeUnassign.errors.length === 0) { notify({ - text: i18n.t("Attribute deleted", { - context: "notification" - }) - }); - } - }; - const handleAttributeUpdateSuccess = (data: AttributeUpdate) => { - if (data.attributeUpdate.errors.length === 0) { - notify({ - text: i18n.t("Attribute updated", { + text: i18n.t("Attribute unassigned", { context: "notification" }) }); closeModal(); - } else if ( - data.attributeUpdate.errors !== null && - data.attributeUpdate.errors.length > 0 - ) { - setErrors.editAttributeErrors(data.attributeUpdate.errors); + productAttributeListActions.reset(); + variantAttributeListActions.reset(); } }; const handleProductTypeDeleteSuccess = ( @@ -126,18 +119,19 @@ export const ProductTypeUpdate: React.StatelessComponent< return ( data.productType)} + onAssignAttribute={handleAttributeAssignSuccess} + onUnassignAttribute={handleAttributeUnassignSuccess} onProductTypeDelete={handleProductTypeDeleteSuccess} onProductTypeUpdate={handleProductTypeUpdateSuccess} + onProductTypeAttributeReorder={() => undefined} > {({ - attributeCreate, - deleteAttribute, + assignAttribute, deleteProductType, - updateAttribute, - updateProductType + unassignAttribute, + updateProductType, + reorderAttribute }) => { const handleProductTypeDelete = () => deleteProductType.mutate({ id }); @@ -153,7 +147,7 @@ export const ProductTypeUpdate: React.StatelessComponent< productAttributes: formData.productAttributes.map( choice => choice.value ), - taxRate: formData.taxRate, + taxCode: formData.taxType, variantAttributes: formData.variantAttributes.map( choice => choice.value ), @@ -161,60 +155,45 @@ export const ProductTypeUpdate: React.StatelessComponent< } }); }; - const handleAttributeCreate = ( - data: AttributeForm, - type: AttributeTypeEnum - ) => - attributeCreate.mutate({ + const handleAssignAttribute = () => + assignAttribute.mutate({ id, - input: { - name: data.name, - values: data.values.map(value => ({ - name: value.label - })) - }, - type + operations: params.ids.map(id => ({ + id, + type: AttributeTypeEnum[params.type] + })) }); - const handleAttributeDelete = ( - id: string, - event: React.MouseEvent - ) => { - event.stopPropagation(); - deleteAttribute.mutate({ id }); - }; - const handleAttributeUpdate = ( - id: string, - formData: AttributeForm - ) => { - const attribute = data.productType.variantAttributes - .concat(data.productType.productAttributes) - .filter(attribute => attribute.id === id)[0]; - updateAttribute.mutate({ + + const handleAttributeUnassign = () => + unassignAttribute.mutate({ id, - input: { - addValues: formData.values - .filter( - value => - !attribute.values - .map(value => value.id) - .includes(value.value) - ) - .map(value => ({ - name: value.label - })), - name: formData.name, - removeValues: attribute.values - .filter( - value => - !formData.values - .map(value => value.value) - .includes(value.id) - ) - .map(value => value.id) - } + ids: [params.id] }); - }; + + const handleBulkAttributeUnassign = () => + unassignAttribute.mutate({ + id, + ids: params.ids + }); + const loading = updateProductType.opts.loading || dataLoading; + + const assignTransactionState = getMutationState( + assignAttribute.opts.called, + assignAttribute.opts.loading, + maybe( + () => assignAttribute.opts.data.attributeAssign.errors + ) + ); + + const unassignTransactionState = getMutationState( + unassignAttribute.opts.called, + unassignAttribute.opts.loading, + maybe( + () => unassignAttribute.opts.data.attributeUnassign.errors + ) + ); + const deleteTransactionState = getMutationState( deleteProductType.opts.called, deleteProductType.opts.loading, @@ -223,12 +202,33 @@ export const ProductTypeUpdate: React.StatelessComponent< ) ); - const attribute = maybe(() => - data.productType.productAttributes - .concat(data.productType.variantAttributes) - .find(attribute => attribute.id === params.id) + const formTransitionState = getMutationState( + updateProductType.opts.called, + updateProductType.opts.loading, + maybe( + () => updateProductType.opts.data.productTypeUpdate.errors + ) ); + const handleAttributeReorder = ( + event: ReorderEvent, + type: AttributeTypeEnum + ) => { + const attributes = + type === AttributeTypeEnum.PRODUCT + ? data.productType.productAttributes + : data.productType.variantAttributes; + + reorderAttribute.mutate({ + move: { + id: attributes[event.oldIndex].id, + sortOrder: event.newIndex - event.oldIndex + }, + productTypeId: id, + type + }); + }; + return ( <> data.productType.name)} /> @@ -240,20 +240,24 @@ export const ProductTypeUpdate: React.StatelessComponent< errors={errors.formErrors} pageTitle={maybe(() => data.productType.name)} productType={maybe(() => data.productType)} - saveButtonBarState={loading ? "loading" : "default"} + saveButtonBarState={formTransitionState} + taxTypes={maybe(() => data.taxTypes, [])} onAttributeAdd={type => navigate( productTypeUrl(id, { - action: "add-attribute", + action: "assign-attribute", type }) ) } - onAttributeDelete={handleAttributeDelete} - onAttributeUpdate={attributeId => + onAttributeClick={attributeId => + navigate(attributeUrl(attributeId)) + } + onAttributeReorder={handleAttributeReorder} + onAttributeUnassign={attributeId => navigate( productTypeUrl(id, { - action: "edit-attribute", + action: "unassign-attribute", id: attributeId }) ) @@ -267,75 +271,200 @@ export const ProductTypeUpdate: React.StatelessComponent< ) } onSubmit={handleProductTypeUpdate} + productAttributeList={{ + isChecked: productAttributeListActions.isSelected, + selected: + productAttributeListActions.listElements.length, + toggle: productAttributeListActions.toggle, + toggleAll: productAttributeListActions.toggleAll, + toolbar: ( + + ) + }} + variantAttributeList={{ + isChecked: variantAttributeListActions.isSelected, + selected: + variantAttributeListActions.listElements.length, + toggle: variantAttributeListActions.toggle, + toggleAll: variantAttributeListActions.toggleAll, + toolbar: ( + + ) + }} /> {!dataLoading && ( - <> - {Object.keys(AttributeTypeEnum).map(key => ( - - handleAttributeCreate( - data, - AttributeTypeEnum[key] - ) - } - opened={ - params.action === "add-attribute" && - params.type === AttributeTypeEnum[key] - } - title={i18n.t("Add Attribute", { - context: "modal title" - })} - key={key} - /> - ))} - attribute.name)} - values={maybe(() => - attribute.values.map(value => ({ - label: value.name, - value: value.id - })) - )} - onClose={closeModal} - onConfirm={data => - handleAttributeUpdate(params.id, data) - } - opened={params.action === "edit-attribute"} - title={i18n.t("Edit Attribute", { - context: "modal title" - })} - /> - navigate(productTypeUrl(id))} - onConfirm={handleProductTypeDelete} - title={i18n.t("Remove product type")} - variant="delete" - > - {{ name }}?", - { - name: maybe( - () => data.productType.name, - "..." - ) + + {({ search, result }) => { + const fetchMore = () => + result.loadMore( + (prev, next) => { + if ( + prev.productType.availableAttributes + .pageInfo.endCursor === + next.productType.availableAttributes + .pageInfo.endCursor + ) { + return prev; } - ) - }} - /> - - + 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 + } + ); + + return ( + <> + {Object.keys(AttributeTypeEnum).map(key => ( + + result.data.productType.availableAttributes.edges.map( + edge => edge.node + ) + )} + confirmButtonState={assignTransactionState} + errors={maybe( + () => + assignAttribute.opts.data.attributeAssign.errors.map( + 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} + /> + ))} + + ); + }} + )} + data.productType.name, "...")} + open={params.action === "remove"} + onClose={() => navigate(productTypeUrl(id))} + onConfirm={handleProductTypeDelete} + /> + params.ids.length.toString(), + "..." + )} + confirmButtonState={unassignTransactionState} + onClose={closeModal} + onConfirm={handleBulkAttributeUnassign} + open={params.action === "unassign-attributes"} + productTypeName={maybe( + () => data.productType.name, + "..." + )} + /> + + [ + ...data.productType.productAttributes, + ...data.productType.variantAttributes + ].find(attribute => attribute.id === params.id) + .name, + "..." + )} + confirmButtonState={unassignTransactionState} + onClose={closeModal} + onConfirm={handleAttributeUnassign} + open={params.action === "unassign-attribute"} + productTypeName={maybe( + () => data.productType.name, + "..." + )} + /> ); }} diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index 216e575a4..cd9724688 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -1,4 +1,4 @@ -import { RawDraftContentState } from "draft-js"; +import { ContentState, convertToRaw, RawDraftContentState } from "draft-js"; import React from "react"; import AppHeader from "@saleor/components/AppHeader"; @@ -7,62 +7,63 @@ import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; +import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; import VisibilityCard from "@saleor/components/VisibilityCard"; +import { SearchCategories_categories_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories"; +import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections"; +import useFormset from "@saleor/hooks/useFormset"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { + getChoices, + ProductAttributeValueChoices, + ProductType +} from "@saleor/products/utils/data"; +import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import i18n from "../../../i18n"; -import { maybe } from "../../../misc"; import { UserError } from "../../../types"; import { ProductCreateData_productTypes_edges_node_productAttributes } from "../../types/ProductCreateData"; +import { + createAttributeChangeHandler, + createAttributeMultiChangeHandler, + createProductTypeSelectHandler +} from "../../utils/handlers"; +import ProductAttributes, { + ProductAttributeInput, + ProductAttributeInputData +} from "../ProductAttributes"; import ProductDetailsForm from "../ProductDetailsForm"; import ProductOrganization from "../ProductOrganization"; import ProductPricing from "../ProductPricing"; import ProductStock from "../ProductStock"; -interface ChoiceType { - label: string; - value: string; -} -export interface FormData { - attributes: Array<{ - slug: string; - value: string; - }>; +interface FormData { basePrice: number; publicationDate: string; - category: ChoiceType; + category: string; + collections: string[]; chargeTaxes: boolean; - collections: ChoiceType[]; description: RawDraftContentState; isPublished: boolean; name: string; - productType: { - label: string; - value: { - hasVariants: boolean; - id: string; - name: string; - productAttributes: ProductCreateData_productTypes_edges_node_productAttributes[]; - }; - }; + productType: string; seoDescription: string; seoTitle: string; sku: string; stockQuantity: number; } +export interface ProductCreatePageSubmitData extends FormData { + attributes: ProductAttributeInput[]; +} interface ProductCreatePageProps { errors: UserError[]; - collections?: Array<{ - id: string; - name: string; - }>; + collections: SearchCollections_collections_edges_node[]; + categories: SearchCategories_categories_edges_node[]; currency: string; - categories?: Array<{ - id: string; - name: string; - }>; disabled: boolean; productTypes?: Array<{ id: string; @@ -76,7 +77,7 @@ interface ProductCreatePageProps { fetchCollections: (data: string) => void; onAttributesEdit: () => void; onBack?(); - onSubmit?(data: FormData); + onSubmit?(data: ProductCreatePageSubmitData); } export const ProductCreatePage: React.StatelessComponent< @@ -84,38 +85,34 @@ export const ProductCreatePage: React.StatelessComponent< > = ({ currency, disabled, - categories, - collections, + categories: categoryChoiceList, + collections: collectionChoiceList, errors: userErrors, fetchCategories, fetchCollections, header, - productTypes, + productTypes: productTypeChoiceList, saveButtonBarState, onBack, onSubmit }: ProductCreatePageProps) => { + // Form values + const { + change: changeAttributeData, + data: attributes, + set: setAttributeData + } = useFormset([]); + + const initialDescription = convertToRaw(ContentState.createFromText("")); const initialData: FormData = { - attributes: [], basePrice: 0, - category: { - label: "", - value: "" - }, + category: "", chargeTaxes: false, collections: [], description: {} as any, isPublished: false, name: "", - productType: { - label: "", - value: { - hasVariants: false, - id: "", - name: "", - productAttributes: [] as ProductCreateData_productTypes_edges_node_productAttributes[] - } - }, + productType: "", publicationDate: "", seoDescription: "", seoTitle: "", @@ -123,16 +120,84 @@ export const ProductCreatePage: React.StatelessComponent< stockQuantity: null }; + // Display values + const [selectedAttributes, setSelectedAttributes] = useStateFromProps< + ProductAttributeValueChoices[] + >([]); + + const [selectedCategory, setSelectedCategory] = useStateFromProps(""); + + const [selectedCollections, setSelectedCollections] = useStateFromProps< + MultiAutocompleteChoiceType[] + >([]); + + const [productType, setProductType] = React.useState({ + hasVariants: false, + id: "", + name: "", + productAttributes: [] as ProductCreateData_productTypes_edges_node_productAttributes[] + }); + + const categories = getChoices(categoryChoiceList); + const collections = getChoices(collectionChoiceList); + const productTypes = getChoices(productTypeChoiceList); + + const handleSubmit = (data: FormData) => + onSubmit({ + attributes, + ...data + }); + return ( - {({ change, data, errors, hasChanged, submit }) => { - const hasVariants = - data.productType && data.productType.value.hasVariants; + {({ + change, + data, + errors, + hasChanged, + submit, + triggerChange, + toggleValue + }) => { + const handleCollectionSelect = createMultiAutocompleteSelectHandler( + toggleValue, + setSelectedCollections, + selectedCollections, + collections + ); + const handleCategorySelect = createSingleAutocompleteSelectHandler( + change, + setSelectedCategory, + categories + ); + const handleAttributeChange = createAttributeChangeHandler( + changeAttributeData, + setSelectedAttributes, + selectedAttributes, + attributes, + triggerChange + ); + const handleAttributeMultiChange = createAttributeMultiChangeHandler( + changeAttributeData, + setSelectedAttributes, + selectedAttributes, + attributes, + triggerChange + ); + + const handleProductTypeSelect = createProductTypeSelectHandler( + change, + setAttributeData, + setSelectedAttributes, + setProductType, + productTypeChoiceList + ); + return ( {i18n.t("Products")} @@ -143,9 +208,17 @@ export const ProductCreatePage: React.StatelessComponent< data={data} disabled={disabled} errors={errors} + initialDescription={initialDescription} onChange={change} /> + + - {!hasVariants && ( + {!productType.hasVariants && ( <> - categories.map(category => ({ - label: category.name, - value: category.id - })), - [] - )} + categories={categories} + categoryInputDisplayValue={selectedCategory} + collections={collections} + data={data} + disabled={disabled} errors={errors} fetchCategories={fetchCategories} fetchCollections={fetchCollections} - collections={maybe( - () => - collections.map(collection => ({ - label: collection.name, - value: collection.id - })), - [] - )} + productType={productType} + productTypeInputDisplayValue={productType.name} productTypes={productTypes} - data={data} - disabled={disabled} - onChange={change} + onCategoryChange={handleCategorySelect} + onCollectionChange={handleCollectionSelect} + onProductTypeChange={handleProductTypeSelect} + collectionsInputDisplayValue={selectedCollections} /> createStyles({ @@ -28,10 +25,16 @@ const styles = (theme: Theme) => }); interface ProductDetailsFormProps extends WithStyles { - data: CreateFormData & UpdateFormData; + data: { + description: RawDraftContentState; + name: string; + }; disabled?: boolean; errors: { [key: string]: string }; - product?: ProductDetails_product; + // Draftail isn't controlled - it needs only initial input + // because it's autosaving on its own. + // Ref https://github.com/mirumee/saleor/issues/4470 + initialDescription: RawDraftContentState; onChange(event: any); } @@ -43,7 +46,7 @@ export const ProductDetailsForm = withStyles(styles, { data, disabled, errors, - product, + initialDescription, onChange }: ProductDetailsFormProps) => ( @@ -67,7 +70,7 @@ export const ProductDetailsForm = withStyles(styles, { disabled={disabled} error={!!errors.descriptionJson} helperText={errors.descriptionJson} - initial={maybe(() => JSON.parse(product.descriptionJson), null)} + initial={initialDescription} label={i18n.t("Description")} name="description" onChange={onChange} diff --git a/src/products/components/ProductImageNavigation/ProductImageNavigation.tsx b/src/products/components/ProductImageNavigation/ProductImageNavigation.tsx index ad9d8937c..452e3bc7e 100644 --- a/src/products/components/ProductImageNavigation/ProductImageNavigation.tsx +++ b/src/products/components/ProductImageNavigation/ProductImageNavigation.tsx @@ -6,7 +6,7 @@ import { withStyles, WithStyles } from "@material-ui/core/styles"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import CardTitle from "@saleor/components/CardTitle"; diff --git a/src/products/components/ProductImages/ProductImages.tsx b/src/products/components/ProductImages/ProductImages.tsx index a4727eca5..0f259f6a4 100644 --- a/src/products/components/ProductImages/ProductImages.tsx +++ b/src/products/components/ProductImages/ProductImages.tsx @@ -12,6 +12,7 @@ import { import CardTitle from "@saleor/components/CardTitle"; import ImageTile from "@saleor/components/ImageTile"; import ImageUpload from "@saleor/components/ImageUpload"; +import { ReorderAction } from "@saleor/types"; import React from "react"; import { SortableContainer, SortableElement } from "react-sortable-hoc"; import i18n from "../../../i18n"; @@ -113,17 +114,21 @@ interface ProductImagesProps extends WithStyles { loading?: boolean; onImageDelete: (id: string) => () => void; onImageEdit: (id: string) => () => void; + onImageReorder?: ReorderAction; onImageUpload(file: File); - onImageReorder?(event: { oldIndex: number; newIndex: number }); } -interface ImageListContainerProps extends WithStyles { - items: any; - onImageDelete: (id: string) => () => void; - onImageEdit: (id: string) => () => void; +interface SortableImageProps { + image: { + id: string; + alt?: string; + url: string; + }; + onImageEdit: (id: string) => void; + onImageDelete: () => void; } -const SortableImage = SortableElement( +const SortableImage = SortableElement( ({ image, onImageEdit, onImageDelete }) => ( () => void; + onImageEdit: (id: string) => () => void; +} + +const ImageListContainer = SortableContainer( withStyles(styles, { name: "ImageListContainer" })( ({ classes, @@ -141,7 +153,7 @@ const ImageListContainer = SortableContainer( onImageDelete, onImageEdit, ...props - }: ImageListContainerProps) => { + }: ImageListContainerProps & WithStyles) => { return (
{items.map((image, index) => ( diff --git a/src/products/components/ProductOrganization/ProductOrganization.tsx b/src/products/components/ProductOrganization/ProductOrganization.tsx index 2c757074f..31e010bba 100644 --- a/src/products/components/ProductOrganization/ProductOrganization.tsx +++ b/src/products/components/ProductOrganization/ProductOrganization.tsx @@ -11,24 +11,23 @@ import React from "react"; import CardSpacer from "@saleor/components/CardSpacer"; import CardTitle from "@saleor/components/CardTitle"; -import Chip from "@saleor/components/Chip"; import { FormSpacer } from "@saleor/components/FormSpacer"; -import MultiAutocompleteSelectField from "@saleor/components/MultiAutocompleteSelectField"; -import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField"; -import Skeleton from "@saleor/components/Skeleton"; -import i18n from "../../../i18n"; -import { maybe } from "../../../misc"; -import { ProductCreateData_productTypes_edges_node_productAttributes } from "../../types/ProductCreateData"; +import Hr from "@saleor/components/Hr"; +import MultiAutocompleteSelectField, { + MultiAutocompleteChoiceType +} from "@saleor/components/MultiAutocompleteSelectField"; +import SingleAutocompleteSelectField, { + SingleAutocompleteChoiceType +} from "@saleor/components/SingleAutocompleteSelectField"; +import { ChangeEvent } from "@saleor/hooks/useForm"; +import i18n from "@saleor/i18n"; +import { maybe } from "@saleor/misc"; +import { FormErrors } from "@saleor/types"; -interface ChoiceType { - label: string; - value: string; -} interface ProductType { hasVariants: boolean; id: string; name: string; - productAttributes: ProductCreateData_productTypes_edges_node_productAttributes[]; } const styles = (theme: Theme) => @@ -40,12 +39,6 @@ const styles = (theme: Theme) => fontSize: "1rem", marginBottom: theme.spacing.unit / 2 }, - hr: { - backgroundColor: theme.overrides.MuiCard.root.borderColor, - border: "none", - height: 1, - margin: `0 -${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px` - }, label: { marginBottom: theme.spacing.unit / 2 } @@ -53,267 +46,116 @@ const styles = (theme: Theme) => interface ProductOrganizationProps extends WithStyles { canChangeType: boolean; - categories?: Array<{ value: string; label: string }>; - collections?: Array<{ value: string; label: string }>; + categories?: SingleAutocompleteChoiceType[]; + categoryInputDisplayValue: string; + collections?: MultiAutocompleteChoiceType[]; + collectionsInputDisplayValue: MultiAutocompleteChoiceType[]; data: { - attributes: Array<{ - slug: string; - value: string; - }>; - category: ChoiceType; - collections: ChoiceType[]; - productType: { - label: string; - value: { - hasVariants: boolean; - id: string; - name: string; - productAttributes: ProductCreateData_productTypes_edges_node_productAttributes[]; - }; - }; + category: string; + collections: string[]; + productType?: string; }; disabled: boolean; - errors: { [key: string]: string }; - product?: { - productType?: { - hasVariants?: boolean; - name?: string; - }; - }; - productTypes?: ProductType[]; + errors: FormErrors<"productType" | "category">; + productType?: ProductType; + productTypeInputDisplayValue?: string; + productTypes?: SingleAutocompleteChoiceType[]; fetchCategories: (query: string) => void; fetchCollections: (query: string) => void; - onChange: (event: React.ChangeEvent, cb?: () => void) => void; + onCategoryChange: (event: ChangeEvent) => void; + onCollectionChange: (event: ChangeEvent) => void; + onProductTypeChange?: (event: ChangeEvent) => void; } const ProductOrganization = withStyles(styles, { name: "ProductOrganization" })( ({ canChangeType, categories, + categoryInputDisplayValue, classes, collections, + collectionsInputDisplayValue, data, disabled, errors, fetchCategories, fetchCollections, - product, + productType, + productTypeInputDisplayValue, productTypes, - onChange - }: ProductOrganizationProps) => { - const unrolledAttributes = maybe( - () => data.productType.value.productAttributes, - [] - ); - const getAttributeName = (slug: string) => { - const match = unrolledAttributes.find(a => a.slug === slug); - if (!match) { - return ""; - } - return match.name; - }; - const getAttributeValue = (slug: string) => { - if (unrolledAttributes.length > 0) { - const value = data.attributes.find(a => a.slug === slug); - const attributeMatch = unrolledAttributes.find(a => a.slug === slug); - if (!attributeMatch) { - return { - label: "", - value: "" - }; - } - const attributeValueMatch = attributeMatch.values.find( - v => v.slug === value.value - ); - const label = !!attributeValueMatch - ? attributeValueMatch.name - : value.value; - return { - label, - value - }; - } - return { - label: "", - value: "" - }; - }; - const getAttributeValues = (slug: string) => { - const match = unrolledAttributes.find(a => a.slug === slug); - if (match) { - return match.values; - } - - return []; - }; - const handleProductTypeSelect = ( - event: React.ChangeEvent<{ - name: string; - value: { - label: string; - value: ProductType; - }; - }> - ) => { - onChange(event, () => - onChange({ - ...event, - target: { - ...event.target, - name: "attributes", - value: event.target.value.value.productAttributes.map( - attribute => ({ - slug: attribute.slug, - value: "" - }) - ) - } - }) - ); - }; - const handleAttributeValueSelect = ( - event: React.ChangeEvent<{ - name: string; - value: { - label: string; - value: string; - }; - }> - ) => { - onChange({ - ...event, - target: { - ...event.target, - name: "attributes", - value: data.attributes.map(a => - a.slug === event.target.name - ? { slug: a.slug, value: event.target.value.value } - : a - ) - } - }); - }; - return ( - - - - {canChangeType ? ( - ({ label: pt.name, value: pt })) - : [] - } - value={data.productType} - onChange={handleProductTypeSelect} - /> - ) : ( - <> - - {i18n.t("Product Type")} - - - {maybe(() => product.productType.name, "...")} - - - - {i18n.t("Product Type")} - - - {maybe( - () => - product.productType.hasVariants - ? i18n.t("Configurable") - : i18n.t("Simple"), - "..." - )} - - - )} - {!(data && data.attributes && data.attributes.length === 0) ? ( - <> - - - {i18n.t("Attributes")} - -
- - ) : ( - - )} - {data.attributes ? ( - data.attributes.map((item, index) => { - return ( - - ({ - label: v.name, - value: v.slug - }))} - custom - /> - - - ); - }) - ) : ( - - )} -
+ onCategoryChange, + onCollectionChange, + onProductTypeChange + }: ProductOrganizationProps) => ( + + + + {canChangeType ? ( - -
- - {({ deleteItem, items }) => ( - <> - -
- {items.map(item => ( - deleteItem(item)} - /> - ))} -
- - )} -
-
-
- ); - } + ) : ( + <> + + {i18n.t("Product Type")} + + {maybe(() => productType.name, "...")} + + + {i18n.t("Product Type")} + + + {maybe( + () => + productType.hasVariants + ? i18n.t("Configurable") + : i18n.t("Simple"), + "..." + )} + + + )} + +
+ + + +
+ + +
+
+ ) ); ProductOrganization.displayName = "ProductOrganization"; export default ProductOrganization; diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index 93db6711c..169aa649e 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -11,15 +11,33 @@ import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; import SeoForm from "@saleor/components/SeoForm"; import VisibilityCard from "@saleor/components/VisibilityCard"; -import i18n from "../../../i18n"; -import { maybe } from "../../../misc"; -import { ListActions, UserError } from "../../../types"; +import { SearchCategories_categories_edges_node } from "@saleor/containers/SearchCategories/types/SearchCategories"; +import { SearchCollections_collections_edges_node } from "@saleor/containers/SearchCollections/types/SearchCollections"; +import useFormset from "@saleor/hooks/useFormset"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import i18n from "@saleor/i18n"; +import { maybe } from "@saleor/misc"; +import { ListActions, UserError } from "@saleor/types"; +import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import { ProductDetails_product, - ProductDetails_product_attributes_attribute, ProductDetails_product_images, ProductDetails_product_variants } from "../../types/ProductDetails"; +import { + getAttributeInputFromProduct, + getChoices, + getProductUpdatePageFormData, + getSelectedAttributesFromProduct, + ProductAttributeValueChoices, + ProductUpdatePageFormData +} from "../../utils/data"; +import { + createAttributeChangeHandler, + createAttributeMultiChangeHandler +} from "../../utils/handlers"; +import ProductAttributes, { ProductAttributeInput } from "../ProductAttributes"; import ProductDetailsForm from "../ProductDetailsForm"; import ProductImages from "../ProductImages"; import ProductOrganization from "../ProductOrganization"; @@ -27,25 +45,15 @@ import ProductPricing from "../ProductPricing"; import ProductStock from "../ProductStock"; import ProductVariants from "../ProductVariants"; -interface ProductUpdateProps extends ListActions { +export interface ProductUpdatePageProps extends ListActions { errors: UserError[]; placeholderImage: string; - collections?: Array<{ - id: string; - name: string; - }>; - categories?: Array<{ - id: string; - name: string; - }>; - disabled?: boolean; - productCollections?: Array<{ - id: string; - name: string; - }>; + collections: SearchCollections_collections_edges_node[]; + categories: SearchCategories_categories_edges_node[]; + disabled: boolean; variants: ProductDetails_product_variants[]; - images?: ProductDetails_product_images[]; - product?: ProductDetails_product; + images: ProductDetails_product_images[]; + product: ProductDetails_product; header: string; saveButtonBarState: ConfirmButtonTransitionState; fetchCategories: (query: string) => void; @@ -60,45 +68,16 @@ interface ProductUpdateProps extends ListActions { onImageUpload(file: File); onProductShow?(); onSeoClick?(); - onSubmit?(data: any); + onSubmit?(data: ProductUpdatePageSubmitData); onVariantAdd?(); } -interface ChoiceType { - label: string; - value: string; -} -export interface FormData { - attributes: Array<{ - slug: string; - value: string; - }>; - basePrice: number; - category: ChoiceType | null; - chargeTaxes: boolean; - collections: ChoiceType[]; - description: RawDraftContentState; - isPublished: boolean; - name: string; - productType: { - label: string; - value: { - hasVariants: boolean; - id: string; - name: string; - productAttributes: Array< - Exclude - >; - }; - } | null; - publicationDate: string; - seoDescription: string; - seoTitle: string; - sku: string; - stockQuantity: number; +export interface ProductUpdatePageSubmitData extends ProductUpdatePageFormData { + attributes: ProductAttributeInput[]; + collections: string[]; } -export const ProductUpdate: React.StatelessComponent = ({ +export const ProductUpdatePage: React.FC = ({ disabled, categories: categoryChoiceList, collections: collectionChoiceList, @@ -109,7 +88,6 @@ export const ProductUpdate: React.StatelessComponent = ({ header, placeholderImage, product, - productCollections, saveButtonBarState, variants, onAttributesEdit, @@ -129,186 +107,198 @@ export const ProductUpdate: React.StatelessComponent = ({ toggleAll, toolbar }) => { - const initialData: FormData = { - attributes: maybe( - () => - product.attributes.map(a => ({ - slug: a.attribute.slug, - value: a.value ? a.value.slug : null - })), - [] - ), - basePrice: maybe(() => product.basePrice.amount), - category: maybe(() => ({ - label: product.category.name, - value: product.category.id - })), - chargeTaxes: maybe(() => product.chargeTaxes, false), - collections: productCollections - ? productCollections.map(collection => ({ - label: collection.name, - value: collection.id - })) - : [], - description: maybe(() => JSON.parse(product.descriptionJson)), - isPublished: maybe(() => product.isPublished, false), - name: maybe(() => product.name), - productType: maybe(() => ({ - label: product.productType.name, - value: { - hasVariants: product.productType.hasVariants, - id: product.productType.id, - name: product.productType.name, - productAttributes: product.attributes.map(a => a.attribute) - } - })), - publicationDate: maybe(() => product.publicationDate), - seoDescription: maybe(() => product.seoDescription) || "", - seoTitle: maybe(() => product.seoTitle) || "", - sku: maybe(() => - product.productType.hasVariants - ? undefined - : variants && variants[0] - ? variants[0].sku - : undefined - ), - stockQuantity: maybe(() => - product.productType.hasVariants - ? undefined - : variants && variants[0] - ? variants[0].quantity - : undefined - ) - }; - const categories = - categoryChoiceList !== undefined - ? categoryChoiceList.map(category => ({ - label: category.name, - value: category.id - })) - : []; - const collections = - collectionChoiceList !== undefined - ? collectionChoiceList.map(collection => ({ - label: collection.name, - value: collection.id - })) - : []; - const currency = - product && product.basePrice ? product.basePrice.currency : undefined; - const hasVariants = - product && product.productType && product.productType.hasVariants; + const attributeInput = React.useMemo( + () => getAttributeInputFromProduct(product), + [product] + ); + const { change: changeAttributeData, data: attributes } = useFormset( + attributeInput + ); + + const [selectedAttributes, setSelectedAttributes] = useStateFromProps< + ProductAttributeValueChoices[] + >(getSelectedAttributesFromProduct(product)); + + const [selectedCategory, setSelectedCategory] = useStateFromProps( + maybe(() => product.category.name, "") + ); + + const [selectedCollections, setSelectedCollections] = useStateFromProps( + getChoices(maybe(() => product.collections, [])) + ); + + const initialData = getProductUpdatePageFormData(product, variants); + const initialDescription = maybe(() => + JSON.parse(product.descriptionJson) + ); + + const categories = getChoices(categoryChoiceList); + const collections = getChoices(collectionChoiceList); + const currency = maybe(() => product.basePrice.currency); + const hasVariants = maybe(() => product.productType.hasVariants, false); + + const handleSubmit = (data: ProductUpdatePageFormData) => + onSubmit({ + attributes, + ...data + }); return ( - {({ change, data, errors, hasChanged, submit }) => ( - <> - - {i18n.t("Products")} - - -
- - - - - - - {hasVariants ? ( - - ) : ( - { + const handleCollectionSelect = createMultiAutocompleteSelectHandler( + toggleValue, + setSelectedCollections, + selectedCollections, + collections + ); + const handleCategorySelect = createSingleAutocompleteSelectHandler( + change, + setSelectedCategory, + categories + ); + const handleAttributeChange = createAttributeChangeHandler( + changeAttributeData, + setSelectedAttributes, + selectedAttributes, + attributes, + triggerChange + ); + const handleAttributeMultiChange = createAttributeMultiChangeHandler( + changeAttributeData, + setSelectedAttributes, + selectedAttributes, + attributes, + triggerChange + ); + + return ( + <> + + {i18n.t("Products")} + + +
+ - )} - - - convertFromRaw(data.description) - .getPlainText() - .slice(0, 300) + + + + + + + + {hasVariants ? ( + + ) : ( + )} - loading={disabled} - onClick={onSeoClick} - onChange={change} - /> -
-
- - - -
-
- -
- - )} + + + convertFromRaw(data.description) + .getPlainText() + .slice(0, 300) + )} + loading={disabled} + onClick={onSeoClick} + onChange={change} + /> +
+
+ product.productType)} + onCategoryChange={handleCategorySelect} + onCollectionChange={handleCollectionSelect} + /> + + +
+
+ +
+ + ); + }} ); }; -ProductUpdate.displayName = "ProductUpdate"; -export default ProductUpdate; +ProductUpdatePage.displayName = "ProductUpdatePage"; +export default ProductUpdatePage; diff --git a/src/products/components/ProductVariantAttributes/ProductVariantAttributes.tsx b/src/products/components/ProductVariantAttributes/ProductVariantAttributes.tsx index 1d45faf12..3693e0224 100644 --- a/src/products/components/ProductVariantAttributes/ProductVariantAttributes.tsx +++ b/src/products/components/ProductVariantAttributes/ProductVariantAttributes.tsx @@ -1,139 +1,105 @@ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; -import { - createStyles, - Theme, - withStyles, - WithStyles -} from "@material-ui/core/styles"; import React from "react"; import CardTitle from "@saleor/components/CardTitle"; -import SingleAutocompleteSelectField from "@saleor/components/SingleAutocompleteSelectField"; +import Grid from "@saleor/components/Grid"; +import SingleAutocompleteSelectField, { + SingleAutocompleteChoiceType +} from "@saleor/components/SingleAutocompleteSelectField"; import Skeleton from "@saleor/components/Skeleton"; +import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset"; import i18n from "../../../i18n"; -import { ProductVariant_attributes_attribute } from "../../types/ProductVariant"; +import { ProductVariant_attributes_attribute_values } from "../../types/ProductVariant"; -const styles = (theme: Theme) => - createStyles({ - card: { - overflow: "visible" - }, - grid: { - display: "grid", - gridColumnGap: `${theme.spacing.unit * 2}px`, - gridRowGap: `${theme.spacing.unit * 3}px`, - gridTemplateColumns: "1fr 1fr" - } - }); +export interface VariantAttributeInputData { + values: ProductVariant_attributes_attribute_values[]; +} +export type VariantAttributeInput = FormsetAtomicData< + VariantAttributeInputData, + string +>; -interface ProductVariantAttributesProps extends WithStyles { - attributes?: ProductVariant_attributes_attribute[]; - data: { - attributes?: Array<{ - slug: string; - value: string; - }>; - }; +interface ProductVariantAttributesProps { + attributes: VariantAttributeInput[]; disabled: boolean; - onChange: ( - event: React.ChangeEvent<{ - name: string; - value: Array<{ - slug: string; - value: string; - }>; - }> - ) => void; + errors: Record; + onChange: FormsetChange; } -const ProductVariantAttributes = withStyles(styles, { - name: "ProductVariantAttributes" -})( - ({ - attributes, - classes, - data, - disabled, - onChange - }: ProductVariantAttributesProps) => { - return ( - - - - {attributes === undefined ? ( - - ) : ( - attributes.map((item, index) => { - const getAttributeValue = (slug: string) => { - const valueMatches = attributes.filter(a => a.slug === slug); - if (valueMatches.length > 0) { - const value = data.attributes.filter(a => a.slug === slug)[0] - .value; - const labelMatches = valueMatches[0].values.filter( - v => v.slug === value - ); - const label = - labelMatches.length > 0 ? labelMatches[0].name : value; - return { - label, - value - }; - } - return { - label: "", - value: "" - }; - }; - const getAttributeValues = (slug: string) => { - const matches = attributes.filter(a => a.slug === slug); - return matches.length > 0 - ? matches[0].values.map(v => ({ - label: v.name, - value: v.slug - })) - : []; - }; - const handleAttributeValueSelect = ( - event: React.ChangeEvent<{ - name: string; - value: { - label: string; - value: string; - }; - }> - ) => - onChange({ - ...(event as any), - target: { - ...event.target, - name: "attributes", - value: data.attributes.map(a => - a.slug === event.target.name - ? { slug: a.slug, value: event.target.value.value } - : a - ) - } - }); - - return ( - - ); - }) - )} - - - ); +function getAttributeDisplayValue( + id: string, + slug: string, + attributes: VariantAttributeInput[] +): string { + const attribute = attributes.find(attr => attr.id === id); + const attributeValue = attribute.data.values.find( + value => value.slug === slug + ); + if (!!attributeValue) { + return attributeValue.name; } + + return slug; +} + +function getAttributeValue( + id: string, + attributes: VariantAttributeInput[] +): string { + const attribute = attributes.find(attr => attr.id === id); + return attribute.value; +} + +function getAttributeValueChoices( + id: string, + attributes: VariantAttributeInput[] +): SingleAutocompleteChoiceType[] { + const attribute = attributes.find(attr => attr.id === id); + return attribute.data.values.map(attributeValue => ({ + label: attributeValue.name, + value: attributeValue.slug + })); +} + +const ProductVariantAttributes: React.FC = ({ + attributes, + disabled, + errors, + onChange +}) => ( + + + + + {attributes === undefined ? ( + + ) : ( + attributes.map((attribute, attributeIndex) => { + return ( + onChange(attribute.id, event.target.value)} + value={getAttributeValue(attribute.id, attributes)} + choices={getAttributeValueChoices(attribute.id, attributes)} + allowCustomValues + /> + ); + }) + )} + + + ); ProductVariantAttributes.displayName = "ProductVariantAttributes"; export default ProductVariantAttributes; diff --git a/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx b/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx index 5ea1ef7fb..1c70303b2 100644 --- a/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx +++ b/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx @@ -8,25 +8,36 @@ import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; +import useFormset, { + FormsetChange, + FormsetData +} from "@saleor/hooks/useFormset"; +import { + getVariantAttributeErrors, + getVariantAttributeInputFromProduct +} from "@saleor/products/utils/data"; import i18n from "../../../i18n"; import { maybe } from "../../../misc"; import { UserError } from "../../../types"; import { ProductVariantCreateData_product } from "../../types/ProductVariantCreateData"; -import ProductVariantAttributes from "../ProductVariantAttributes"; +import ProductVariantAttributes, { + VariantAttributeInputData +} from "../ProductVariantAttributes"; import ProductVariantNavigation from "../ProductVariantNavigation"; import ProductVariantPrice from "../ProductVariantPrice"; import ProductVariantStock from "../ProductVariantStock"; -interface FormData { - attributes?: Array<{ - slug: string; - value: string; - }>; - costPrice?: string; - images?: string[]; - priceOverride?: string; - quantity?: number; - sku?: string; +interface ProductVariantCreatePageFormData { + costPrice: string; + images: string[]; + priceOverride: string; + quantity: number; + sku: string; +} + +export interface ProductVariantCreatePageSubmitData + extends ProductVariantCreatePageFormData { + attributes: FormsetData; } interface ProductVariantCreatePageProps { @@ -37,13 +48,11 @@ interface ProductVariantCreatePageProps { product: ProductVariantCreateData_product; saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; - onSubmit: (data: FormData) => void; + onSubmit: (data: ProductVariantCreatePageSubmitData) => void; onVariantClick: (variantId: string) => void; } -const ProductVariantCreatePage: React.StatelessComponent< - ProductVariantCreatePageProps -> = ({ +const ProductVariantCreatePage: React.FC = ({ currencySymbol, errors: formErrors, loading, @@ -54,12 +63,23 @@ const ProductVariantCreatePage: React.StatelessComponent< onSubmit, onVariantClick }) => { + const attributeInput = React.useMemo( + () => getVariantAttributeInputFromProduct(product), + [product] + ); + const { change: changeAttributeData, data: attributes } = useFormset( + attributeInput + ); + const initialForm = { - attributes: maybe(() => - product.productType.variantAttributes.map(attribute => ({ - slug: attribute.slug, - value: "" - })) + attributes: maybe( + () => + product.productType.variantAttributes.map(attribute => ({ + name: attribute.name, + slug: attribute.slug, + values: [""] + })), + [] ), costPrice: "", images: maybe(() => product.images.map(image => image.id)), @@ -67,62 +87,79 @@ const ProductVariantCreatePage: React.StatelessComponent< quantity: 0, sku: "" }; + + const handleSubmit = (data: ProductVariantCreatePageFormData) => + onSubmit({ + ...data, + attributes + }); + return ( -
- {({ change, data, errors, hasChanged, submit }) => ( - - {maybe(() => product.name)} - - -
- product.thumbnail.url)} - variants={maybe(() => product.variants)} - onRowClick={(variantId: string) => { - if (product && product.variants) { - return onVariantClick(variantId); - } - }} - /> -
-
- product.productType.variantAttributes)} - data={data} - disabled={loading} - onChange={change} - /> - - - - -
-
- -
- )} + + {({ change, data, errors, hasChanged, submit, triggerChange }) => { + const handleAttributeChange: FormsetChange = (id, value) => { + changeAttributeData(id, value); + triggerChange(); + }; + + return ( + + {maybe(() => product.name)} + + +
+ product.thumbnail.url)} + variants={maybe(() => product.variants)} + onRowClick={(variantId: string) => { + if (product && product.variants) { + return onVariantClick(variantId); + } + }} + /> +
+
+ product.productType.variantAttributes) + )} + onChange={handleAttributeChange} + /> + + + + +
+
+ +
+ ); + }} ); }; diff --git a/src/products/components/ProductVariantImageSelectDialog/ProductVariantImageSelectDialog.tsx b/src/products/components/ProductVariantImageSelectDialog/ProductVariantImageSelectDialog.tsx index 1708e288e..028a5eacd 100644 --- a/src/products/components/ProductVariantImageSelectDialog/ProductVariantImageSelectDialog.tsx +++ b/src/products/components/ProductVariantImageSelectDialog/ProductVariantImageSelectDialog.tsx @@ -9,7 +9,7 @@ import { withStyles, WithStyles } from "@material-ui/core/styles"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import i18n from "../../../i18n"; diff --git a/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx b/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx index 45252d754..96f534a15 100644 --- a/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx +++ b/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx @@ -10,7 +10,7 @@ import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import TableRow from "@material-ui/core/TableRow"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import CardTitle from "@saleor/components/CardTitle"; @@ -68,35 +68,27 @@ const ProductVariantNavigation = withStyles(styles, {
- {renderCollection( - variants, - variant => ( - onRowClick(variant.id) : undefined} - > - variant.images[0].url, - fallbackThumbnail - )} - /> - - {variant ? variant.name || variant.sku : } - - - ), - () => ( - - {i18n.t("This product has no variants")} - - ) - )} + {renderCollection(variants, variant => ( + onRowClick(variant.id) : undefined} + > + variant.images[0].url, + fallbackThumbnail + )} + /> + + {variant ? variant.name || variant.sku : } + + + ))} {onAdd ? ( diff --git a/src/products/components/ProductVariantPage/ProductVariantPage.tsx b/src/products/components/ProductVariantPage/ProductVariantPage.tsx index 1dd8be01e..56e3b6499 100644 --- a/src/products/components/ProductVariantPage/ProductVariantPage.tsx +++ b/src/products/components/ProductVariantPage/ProductVariantPage.tsx @@ -8,16 +8,38 @@ import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; +import useFormset, { + FormsetChange, + FormsetData +} from "@saleor/hooks/useFormset"; +import { + getAttributeInputFromVariant, + getVariantAttributeErrors +} from "@saleor/products/utils/data"; import { maybe } from "../../../misc"; import { UserError } from "../../../types"; import { ProductVariant } from "../../types/ProductVariant"; -import ProductVariantAttributes from "../ProductVariantAttributes"; +import ProductVariantAttributes, { + VariantAttributeInputData +} from "../ProductVariantAttributes"; import ProductVariantImages from "../ProductVariantImages"; import ProductVariantImageSelectDialog from "../ProductVariantImageSelectDialog"; import ProductVariantNavigation from "../ProductVariantNavigation"; import ProductVariantPrice from "../ProductVariantPrice"; import ProductVariantStock from "../ProductVariantStock"; +export interface ProductVariantPageFormData { + costPrice: string; + priceOverride: string; + quantity: number; + sku: string; +} + +export interface ProductVariantPageSubmitData + extends ProductVariantPageFormData { + attributes: FormsetData; +} + interface ProductVariantPageProps { variant?: ProductVariant; errors: UserError[]; @@ -28,12 +50,12 @@ interface ProductVariantPageProps { onAdd(); onBack(); onDelete(); - onSubmit(data: any); + onSubmit(data: ProductVariantPageSubmitData); onImageSelect(id: string); onVariantClick(variantId: string); } -const ProductVariantPage: React.StatelessComponent = ({ +const ProductVariantPage: React.FC = ({ errors: formErrors, loading, header, @@ -47,20 +69,42 @@ const ProductVariantPage: React.StatelessComponent = ({ onSubmit, onVariantClick }) => { + const attributeInput = React.useMemo( + () => getAttributeInputFromVariant(variant), + [variant] + ); + const { change: changeAttributeData, data: attributes } = useFormset( + attributeInput + ); + const [isModalOpened, setModalStatus] = React.useState(false); const toggleModal = () => setModalStatus(!isModalOpened); - const variantImages = variant ? variant.images.map(image => image.id) : []; - const productImages = variant - ? variant.product.images.sort((prev, next) => - prev.sortOrder > next.sortOrder ? 1 : -1 - ) - : undefined; - const images = productImages - ? productImages - .filter(image => variantImages.indexOf(image.id) !== -1) - .sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1)) - : undefined; + const variantImages = maybe(() => variant.images.map(image => image.id), []); + const productImages = maybe(() => + variant.product.images.sort((prev, next) => + prev.sortOrder > next.sortOrder ? 1 : -1 + ) + ); + const images = maybe(() => + productImages + .filter(image => variantImages.indexOf(image.id) !== -1) + .sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1)) + ); + + const initialForm: ProductVariantPageFormData = { + costPrice: maybe(() => variant.costPrice.amount.toString(), ""), + priceOverride: maybe(() => variant.priceOverride.amount.toString(), ""), + quantity: maybe(() => variant.quantity, 0), + sku: maybe(() => variant.sku, "") + }; + + const handleSubmit = (data: ProductVariantPageFormData) => + onSubmit({ + ...data, + attributes + }); + return ( <> @@ -69,102 +113,94 @@ const ProductVariantPage: React.StatelessComponent = ({
({ - slug: a.attribute.slug, - value: a.value.slug - })) - : [], - costPrice: - variant && variant.costPrice - ? variant.costPrice.amount.toString() - : null, - priceOverride: - variant && variant.priceOverride - ? variant.priceOverride.amount.toString() - : null, - quantity: variant && variant.quantity ? variant.quantity : "", - sku: variant && variant.sku - }} + initial={initialForm} errors={formErrors} - onSubmit={onSubmit} + onSubmit={handleSubmit} confirmLeave > - {({ change, data, errors, hasChanged, submit }) => ( - <> - -
- variant.product.thumbnail.url - )} - variants={maybe(() => variant.product.variants)} - onAdd={onAdd} - onRowClick={(variantId: string) => { - if (variant) { - return onVariantClick(variantId); + {({ change, data, errors, hasChanged, submit, triggerChange }) => { + const handleAttributeChange: FormsetChange = (id, value) => { + changeAttributeData(id, value); + triggerChange(); + }; + + return ( + <> + +
+ variant.product.thumbnail.url + )} + variants={maybe(() => variant.product.variants)} + onAdd={onAdd} + onRowClick={(variantId: string) => { + if (variant) { + return onVariantClick(variantId); + } + }} + /> +
+
+ + variant.attributes.map( + attribute => attribute.attribute + ) + ) + )} + onChange={handleAttributeChange} + /> + + + + -
-
- a.attribute) - : undefined - } - data={data} - disabled={loading} - onChange={change} - /> - - - - - - -
-
- - - )} + costPrice={data.costPrice} + loading={loading} + onChange={change} + /> + + +
+
+ + + ); + }}
{variant && ( diff --git a/src/products/components/ProductVariants/ProductVariants.tsx b/src/products/components/ProductVariants/ProductVariants.tsx index 5e7acbe77..6784d352d 100644 --- a/src/products/components/ProductVariants/ProductVariants.tsx +++ b/src/products/components/ProductVariants/ProductVariants.tsx @@ -72,6 +72,8 @@ interface ProductVariantsProps extends ListActions, WithStyles { onVariantAdd?(); } +const numberOfColumns = 5; + export const ProductVariants = withStyles(styles, { name: "ProductVariants" })( ({ classes, @@ -110,6 +112,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })(
toggle(variant.id)} /> @@ -188,7 +192,7 @@ export const ProductVariants = withStyles(styles, { name: "ProductVariants" })( }, () => ( - + {i18n.t("This product has no variants")} diff --git a/src/products/fixtures.ts b/src/products/fixtures.ts index 2fc48bc95..0ae25921b 100644 --- a/src/products/fixtures.ts +++ b/src/products/fixtures.ts @@ -1,3 +1,4 @@ +import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; import { content } from "../storybook/stories/components/RichTextEditor"; import { ProductDetails_product } from "./types/ProductDetails"; import { ProductVariant } from "./types/ProductVariant"; @@ -7,87 +8,85 @@ export const product: ( placeholderImage: string ) => ProductDetails_product & ProductVariantCreateData_product = placeholderImage => ({ - __typename: "Product", + __typename: "Product" as "Product", attributes: [ { __typename: "SelectedAttribute", attribute: { - __typename: "Attribute", + __typename: "Attribute" as "Attribute", id: "pta18161", + inputType: AttributeInputTypeEnum.DROPDOWN, name: "Borders", slug: "Borders", - sortNumber: 0, + valueRequired: true, values: [ { __typename: "AttributeValue", id: "ptav47282", name: "portals", - slug: "portals", - sortNumber: 0 + slug: "portals" }, { __typename: "AttributeValue", id: "ptav17253", name: "Baht", - slug: "Baht", - sortNumber: 1 + slug: "Baht" } ] }, - value: { - __typename: "AttributeValue", - id: "ptav47282", - name: "portals", - slug: "portals", - sortNumber: 0 - } + values: [ + { + __typename: "AttributeValue", + id: "ptav47282", + name: "portals", + slug: "portals" + } + ] }, { __typename: "SelectedAttribute", attribute: { - __typename: "Attribute", + __typename: "Attribute" as "Attribute", id: "pta22785", + inputType: AttributeInputTypeEnum.MULTISELECT, name: "Legacy", slug: "Legacy", - sortNumber: 1, + valueRequired: true, values: [ { __typename: "AttributeValue", id: "ptav31282", name: "payment", - slug: "payment", - sortNumber: 0 + slug: "payment" }, { __typename: "AttributeValue", id: "ptav14907", name: "Auto Loan Account", - slug: "Auto-Loan-Account", - sortNumber: 1 + slug: "Auto-Loan-Account" }, { __typename: "AttributeValue", id: "ptav27366", name: "Garden", - slug: "Garden", - sortNumber: 2 + slug: "Garden" }, { __typename: "AttributeValue", id: "ptav11873", name: "override", - slug: "override", - sortNumber: 3 + slug: "override" } ] }, - value: { - __typename: "AttributeValue", - id: "ptav14907", - name: "Auto Loan Account", - slug: "Auto-Loan-Account", - sortNumber: 1 - } + values: [ + { + __typename: "AttributeValue", + id: "ptav14907", + name: "Auto Loan Account", + slug: "Auto-Loan-Account" + } + ] } ], basePrice: { @@ -195,6 +194,7 @@ export const product: ( name: "Color", slug: "color", sortOrder: 0, + valueRequired: true, values: [ { __typename: "AttributeValue", @@ -233,7 +233,7 @@ export const product: ( seoDescription: "Seo description", seoTitle: "Seo title", sku: "59661-34207", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, url: "/example-url", variants: [ { @@ -290,9 +290,11 @@ export const product: ( }); export const products = (placeholderImage: string) => [ { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta37372", name: "expedite", slug: "expedite", @@ -323,6 +325,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta3183", name: "system", slug: "system", @@ -349,6 +352,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta47147", name: "interactive", slug: "interactive", @@ -371,6 +375,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta34499", name: "deposit", slug: "deposit", @@ -418,6 +423,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -445,7 +451,7 @@ export const products = (placeholderImage: string) => [ "Autem odit tempora nesciunt quaerat enim reprehenderit eius. Excepturi nemo quos veritatis laboriosam aperiam atque natus soluta", seoTitle: "Generic Rubber Pants", sku: "57599-97473", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -472,9 +478,11 @@ export const products = (placeholderImage: string) => [ } }, { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta1529", name: "Granite", slug: "Granite", @@ -504,6 +512,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta44120", name: "wireless", slug: "wireless", @@ -540,6 +549,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta83114", name: "6th generation", slug: "6th-generation", @@ -579,6 +589,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -601,7 +612,7 @@ export const products = (placeholderImage: string) => [ seoDescription: "enim est recusandae soluta aperiam", seoTitle: "Handcrafted Wooden Towels", sku: "6526-89350", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -658,9 +669,11 @@ export const products = (placeholderImage: string) => [ } }, { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta40940", name: "monitor", slug: "monitor", @@ -707,6 +720,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta33894", name: "Assurance", slug: "Assurance", @@ -743,6 +757,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta35528", name: "Function-based", slug: "Function-based", @@ -776,6 +791,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -798,7 +814,7 @@ export const products = (placeholderImage: string) => [ seoDescription: "quas minima error repudiandae corrupti", seoTitle: "Handcrafted Metal Cheese", sku: "9937-5954", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -845,9 +861,11 @@ export const products = (placeholderImage: string) => [ } }, { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta58079", name: "Cotton", slug: "Cotton", @@ -874,6 +892,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta51731", name: "Assistant", slug: "Assistant", @@ -886,6 +905,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta53013", name: "target", slug: "target", @@ -921,6 +941,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta98307", name: "Brand", slug: "Brand", @@ -943,6 +964,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta52191", name: "invoice", slug: "invoice", @@ -984,6 +1006,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -1007,7 +1030,7 @@ export const products = (placeholderImage: string) => [ "Veniam quasi temporibus ab similique. Praesentium vero repudiandae", seoTitle: "Awesome Granite Car", sku: "96777-48145", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -1084,9 +1107,11 @@ export const products = (placeholderImage: string) => [ } }, { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta96115", name: "Officer", slug: "Officer", @@ -1099,6 +1124,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta74440", name: "driver", slug: "driver", @@ -1119,6 +1145,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta95292", name: "SDD", slug: "SDD", @@ -1147,6 +1174,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta28064", name: "firewall", slug: "firewall", @@ -1165,6 +1193,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta90527", name: "Jamaica", slug: "Jamaica", @@ -1201,6 +1230,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -1224,7 +1254,7 @@ export const products = (placeholderImage: string) => [ "Minima quaerat error incidunt qui quis vitae maxime. Est ab et", seoTitle: "Gorgeous Metal Gloves", sku: "60799-40023", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -1311,9 +1341,11 @@ export const products = (placeholderImage: string) => [ } }, { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta77866", name: "well-modulated", slug: "well-modulated", @@ -1335,6 +1367,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta46984", name: "magenta", slug: "magenta", @@ -1357,6 +1390,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta58329", name: "District", slug: "District", @@ -1380,6 +1414,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta24725", name: "payment", slug: "payment", @@ -1415,6 +1450,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta20828", name: "functionalities", slug: "functionalities", @@ -1457,6 +1493,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -1480,7 +1517,7 @@ export const products = (placeholderImage: string) => [ "Esse quia voluptates veniam eveniet. Velit laborum possimus eveniet consequuntur magnam eveniet provident et voluptatem", seoTitle: "Unbranded Rubber Ball", sku: "2840-48373", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -1567,9 +1604,11 @@ export const products = (placeholderImage: string) => [ } }, { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta66889", name: "Compatible", slug: "Compatible", @@ -1610,6 +1649,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta85893", name: "program", slug: "program", @@ -1652,6 +1692,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -1675,7 +1716,7 @@ export const products = (placeholderImage: string) => [ "Sed tempore autem voluptas voluptas aut sit et. Molestiae rem quo cupiditate consectetur", seoTitle: "Fantastic Cotton Tuna", sku: "63913-27946", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -1722,9 +1763,11 @@ export const products = (placeholderImage: string) => [ } }, { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta3756", name: "directional", slug: "directional", @@ -1761,6 +1804,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -1784,7 +1828,7 @@ export const products = (placeholderImage: string) => [ "Voluptatem veniam aut rerum. Illum veniam asperiores ut et iusto enim vel sapiente quia", seoTitle: "Tasty Steel Pants", sku: "4110-47925", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -1861,9 +1905,11 @@ export const products = (placeholderImage: string) => [ } }, { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta93061", name: "Cotton", slug: "Cotton", @@ -1895,6 +1941,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta58822", name: "Shore", slug: "Shore", @@ -1909,6 +1956,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta33738", name: "Communications", slug: "Communications", @@ -1932,6 +1980,7 @@ export const products = (placeholderImage: string) => [ }, { attribute: { + __typename: "Attribute" as "Attribute", id: "pta46618", name: "Credit Card Account", slug: "Credit-Card-Account", @@ -1968,6 +2017,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -1991,7 +2041,7 @@ export const products = (placeholderImage: string) => [ "Sapiente qui error. Placeat hic nulla repudiandae delectus et", seoTitle: "Ergonomic Cotton Shoes", sku: "9207-4523", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -2058,9 +2108,11 @@ export const products = (placeholderImage: string) => [ } }, { + __typename: "Product" as "Product", attributes: [ { attribute: { + __typename: "Attribute" as "Attribute", id: "pta1842", name: "Small", slug: "Small", @@ -2100,6 +2152,7 @@ export const products = (placeholderImage: string) => [ edges: [ { node: { + __typename: "Image" as "Image", id: "UHJvZHVjdEltYWdlOjE=", image: placeholderImage, sortOrder: 0, @@ -2122,7 +2175,7 @@ export const products = (placeholderImage: string) => [ seoDescription: "adipisci laboriosam autem vel soluta", seoTitle: "Unbranded Steel Tuna", sku: "84653-71539", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: { edges: [ { @@ -2185,10 +2238,11 @@ export const variant = (placeholderImage: string): ProductVariant => ({ { __typename: "SelectedAttribute", attribute: { - __typename: "Attribute", + __typename: "Attribute" as "Attribute", id: "pta18161", name: "Borders", slug: "Borders", + valueRequired: true, values: [ { __typename: "AttributeValue", @@ -2214,10 +2268,11 @@ export const variant = (placeholderImage: string): ProductVariant => ({ { __typename: "SelectedAttribute", attribute: { - __typename: "Attribute", + __typename: "Attribute" as "Attribute", id: "pta22785", name: "Legacy", slug: "Legacy", + valueRequired: true, values: [ { __typename: "AttributeValue", @@ -2288,7 +2343,7 @@ export const variant = (placeholderImage: string): ProductVariant => ({ currency: "USD" }, product: { - __typename: "Product", + __typename: "Product" as "Product", id: "prod1", images: [ { @@ -2356,7 +2411,7 @@ export const variant = (placeholderImage: string): ProductVariant => ({ } ], name: "Our Awesome Book", - thumbnail: { __typename: "Image", url: placeholderImage }, + thumbnail: { __typename: "Image" as "Image", url: placeholderImage }, variants: [ { __typename: "ProductVariant", diff --git a/src/products/mutations.ts b/src/products/mutations.ts index b8845d331..ef7024d69 100644 --- a/src/products/mutations.ts +++ b/src/products/mutations.ts @@ -124,6 +124,7 @@ export const productUpdateMutation = gql` $isPublished: Boolean! $name: String $basePrice: Decimal + $seo: SeoInput ) { productUpdate( id: $id @@ -137,6 +138,7 @@ export const productUpdateMutation = gql` isPublished: $isPublished name: $name basePrice: $basePrice + seo: $seo } ) { errors { @@ -170,6 +172,7 @@ export const simpleProductUpdateMutation = gql` $basePrice: Decimal $productVariantId: ID! $productVariantInput: ProductVariantInput! + $seo: SeoInput ) { productUpdate( id: $id @@ -183,6 +186,7 @@ export const simpleProductUpdateMutation = gql` isPublished: $isPublished name: $name basePrice: $basePrice + seo: $seo } ) { errors { @@ -224,6 +228,7 @@ export const productCreateMutation = gql` $productType: ID! $sku: String $stockQuantity: Int + $seo: SeoInput ) { productCreate( input: { @@ -239,6 +244,7 @@ export const productCreateMutation = gql` productType: $productType sku: $sku quantity: $stockQuantity + seo: $seo } ) { errors { diff --git a/src/products/queries.ts b/src/products/queries.ts index 84c3c345d..6bf4516f7 100644 --- a/src/products/queries.ts +++ b/src/products/queries.ts @@ -95,12 +95,15 @@ export const productFragmentDetails = gql` id slug name + inputType + valueRequired values { + id name slug } } - value { + values { id name slug @@ -154,6 +157,7 @@ export const fragmentVariant = gql` id name slug + valueRequired values { id name @@ -273,11 +277,12 @@ const productCreateQuery = gql` hasVariants productAttributes { id + inputType slug name + valueRequired values { id - sortOrder name slug } @@ -307,9 +312,9 @@ const productVariantCreateQuery = gql` id slug name + valueRequired values { id - sortOrder name slug } diff --git a/src/products/types/Product.ts b/src/products/types/Product.ts index 2396b200b..35d5a5eba 100644 --- a/src/products/types/Product.ts +++ b/src/products/types/Product.ts @@ -2,6 +2,8 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { AttributeInputTypeEnum } from "./../../types/globalTypes"; + // ==================================================== // GraphQL fragment: Product // ==================================================== @@ -50,6 +52,7 @@ export interface Product_purchaseCost { export interface Product_attributes_attribute_values { __typename: "AttributeValue"; + id: string; name: string | null; slug: string | null; } @@ -59,10 +62,12 @@ export interface Product_attributes_attribute { id: string; slug: string | null; name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; values: (Product_attributes_attribute_values | null)[] | null; } -export interface Product_attributes_value { +export interface Product_attributes_values { __typename: "AttributeValue"; id: string; name: string | null; @@ -72,7 +77,7 @@ export interface Product_attributes_value { export interface Product_attributes { __typename: "SelectedAttribute"; attribute: Product_attributes_attribute; - value: Product_attributes_value; + values: (Product_attributes_values | null)[]; } export interface Product_pricing_priceRange_start_net { diff --git a/src/products/types/ProductCreate.ts b/src/products/types/ProductCreate.ts index 2ea047957..bb2706b5f 100644 --- a/src/products/types/ProductCreate.ts +++ b/src/products/types/ProductCreate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeValueInput } from "./../../types/globalTypes"; +import { AttributeValueInput, SeoInput, AttributeInputTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: ProductCreate @@ -58,6 +58,7 @@ export interface ProductCreate_productCreate_product_purchaseCost { export interface ProductCreate_productCreate_product_attributes_attribute_values { __typename: "AttributeValue"; + id: string; name: string | null; slug: string | null; } @@ -67,10 +68,12 @@ export interface ProductCreate_productCreate_product_attributes_attribute { id: string; slug: string | null; name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; values: (ProductCreate_productCreate_product_attributes_attribute_values | null)[] | null; } -export interface ProductCreate_productCreate_product_attributes_value { +export interface ProductCreate_productCreate_product_attributes_values { __typename: "AttributeValue"; id: string; name: string | null; @@ -80,7 +83,7 @@ export interface ProductCreate_productCreate_product_attributes_value { export interface ProductCreate_productCreate_product_attributes { __typename: "SelectedAttribute"; attribute: ProductCreate_productCreate_product_attributes_attribute; - value: ProductCreate_productCreate_product_attributes_value; + values: (ProductCreate_productCreate_product_attributes_values | null)[]; } export interface ProductCreate_productCreate_product_pricing_priceRange_start_net { @@ -196,4 +199,5 @@ export interface ProductCreateVariables { productType: string; sku?: string | null; stockQuantity?: number | null; + seo?: SeoInput | null; } diff --git a/src/products/types/ProductCreateData.ts b/src/products/types/ProductCreateData.ts index a19865341..4e6513b86 100644 --- a/src/products/types/ProductCreateData.ts +++ b/src/products/types/ProductCreateData.ts @@ -2,6 +2,8 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { AttributeInputTypeEnum } from "./../../types/globalTypes"; + // ==================================================== // GraphQL query operation: ProductCreateData // ==================================================== @@ -9,7 +11,6 @@ export interface ProductCreateData_productTypes_edges_node_productAttributes_values { __typename: "AttributeValue"; id: string; - sortOrder: number | null; name: string | null; slug: string | null; } @@ -17,8 +18,10 @@ export interface ProductCreateData_productTypes_edges_node_productAttributes_val export interface ProductCreateData_productTypes_edges_node_productAttributes { __typename: "Attribute"; id: string; + inputType: AttributeInputTypeEnum | null; slug: string | null; name: string | null; + valueRequired: boolean; values: (ProductCreateData_productTypes_edges_node_productAttributes_values | null)[] | null; } diff --git a/src/products/types/ProductDetails.ts b/src/products/types/ProductDetails.ts index 4e0326d16..5094afd59 100644 --- a/src/products/types/ProductDetails.ts +++ b/src/products/types/ProductDetails.ts @@ -2,6 +2,8 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { AttributeInputTypeEnum } from "./../../types/globalTypes"; + // ==================================================== // GraphQL query operation: ProductDetails // ==================================================== @@ -50,6 +52,7 @@ export interface ProductDetails_product_purchaseCost { export interface ProductDetails_product_attributes_attribute_values { __typename: "AttributeValue"; + id: string; name: string | null; slug: string | null; } @@ -59,10 +62,12 @@ export interface ProductDetails_product_attributes_attribute { id: string; slug: string | null; name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; values: (ProductDetails_product_attributes_attribute_values | null)[] | null; } -export interface ProductDetails_product_attributes_value { +export interface ProductDetails_product_attributes_values { __typename: "AttributeValue"; id: string; name: string | null; @@ -72,7 +77,7 @@ export interface ProductDetails_product_attributes_value { export interface ProductDetails_product_attributes { __typename: "SelectedAttribute"; attribute: ProductDetails_product_attributes_attribute; - value: ProductDetails_product_attributes_value; + values: (ProductDetails_product_attributes_values | null)[]; } export interface ProductDetails_product_pricing_priceRange_start_net { diff --git a/src/products/types/ProductImageCreate.ts b/src/products/types/ProductImageCreate.ts index 1e4baa729..db1c34871 100644 --- a/src/products/types/ProductImageCreate.ts +++ b/src/products/types/ProductImageCreate.ts @@ -2,6 +2,8 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { AttributeInputTypeEnum } from "./../../types/globalTypes"; + // ==================================================== // GraphQL mutation operation: ProductImageCreate // ==================================================== @@ -56,6 +58,7 @@ export interface ProductImageCreate_productImageCreate_product_purchaseCost { export interface ProductImageCreate_productImageCreate_product_attributes_attribute_values { __typename: "AttributeValue"; + id: string; name: string | null; slug: string | null; } @@ -65,10 +68,12 @@ export interface ProductImageCreate_productImageCreate_product_attributes_attrib id: string; slug: string | null; name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; values: (ProductImageCreate_productImageCreate_product_attributes_attribute_values | null)[] | null; } -export interface ProductImageCreate_productImageCreate_product_attributes_value { +export interface ProductImageCreate_productImageCreate_product_attributes_values { __typename: "AttributeValue"; id: string; name: string | null; @@ -78,7 +83,7 @@ export interface ProductImageCreate_productImageCreate_product_attributes_value export interface ProductImageCreate_productImageCreate_product_attributes { __typename: "SelectedAttribute"; attribute: ProductImageCreate_productImageCreate_product_attributes_attribute; - value: ProductImageCreate_productImageCreate_product_attributes_value; + values: (ProductImageCreate_productImageCreate_product_attributes_values | null)[]; } export interface ProductImageCreate_productImageCreate_product_pricing_priceRange_start_net { diff --git a/src/products/types/ProductImageUpdate.ts b/src/products/types/ProductImageUpdate.ts index bd2a9d178..045296fa0 100644 --- a/src/products/types/ProductImageUpdate.ts +++ b/src/products/types/ProductImageUpdate.ts @@ -2,6 +2,8 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. +import { AttributeInputTypeEnum } from "./../../types/globalTypes"; + // ==================================================== // GraphQL mutation operation: ProductImageUpdate // ==================================================== @@ -56,6 +58,7 @@ export interface ProductImageUpdate_productImageUpdate_product_purchaseCost { export interface ProductImageUpdate_productImageUpdate_product_attributes_attribute_values { __typename: "AttributeValue"; + id: string; name: string | null; slug: string | null; } @@ -65,10 +68,12 @@ export interface ProductImageUpdate_productImageUpdate_product_attributes_attrib id: string; slug: string | null; name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; values: (ProductImageUpdate_productImageUpdate_product_attributes_attribute_values | null)[] | null; } -export interface ProductImageUpdate_productImageUpdate_product_attributes_value { +export interface ProductImageUpdate_productImageUpdate_product_attributes_values { __typename: "AttributeValue"; id: string; name: string | null; @@ -78,7 +83,7 @@ export interface ProductImageUpdate_productImageUpdate_product_attributes_value export interface ProductImageUpdate_productImageUpdate_product_attributes { __typename: "SelectedAttribute"; attribute: ProductImageUpdate_productImageUpdate_product_attributes_attribute; - value: ProductImageUpdate_productImageUpdate_product_attributes_value; + values: (ProductImageUpdate_productImageUpdate_product_attributes_values | null)[]; } export interface ProductImageUpdate_productImageUpdate_product_pricing_priceRange_start_net { diff --git a/src/products/types/ProductUpdate.ts b/src/products/types/ProductUpdate.ts index 0b8795836..97189239a 100644 --- a/src/products/types/ProductUpdate.ts +++ b/src/products/types/ProductUpdate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeValueInput } from "./../../types/globalTypes"; +import { AttributeValueInput, SeoInput, AttributeInputTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: ProductUpdate @@ -58,6 +58,7 @@ export interface ProductUpdate_productUpdate_product_purchaseCost { export interface ProductUpdate_productUpdate_product_attributes_attribute_values { __typename: "AttributeValue"; + id: string; name: string | null; slug: string | null; } @@ -67,10 +68,12 @@ export interface ProductUpdate_productUpdate_product_attributes_attribute { id: string; slug: string | null; name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; values: (ProductUpdate_productUpdate_product_attributes_attribute_values | null)[] | null; } -export interface ProductUpdate_productUpdate_product_attributes_value { +export interface ProductUpdate_productUpdate_product_attributes_values { __typename: "AttributeValue"; id: string; name: string | null; @@ -80,7 +83,7 @@ export interface ProductUpdate_productUpdate_product_attributes_value { export interface ProductUpdate_productUpdate_product_attributes { __typename: "SelectedAttribute"; attribute: ProductUpdate_productUpdate_product_attributes_attribute; - value: ProductUpdate_productUpdate_product_attributes_value; + values: (ProductUpdate_productUpdate_product_attributes_values | null)[]; } export interface ProductUpdate_productUpdate_product_pricing_priceRange_start_net { @@ -194,4 +197,5 @@ export interface ProductUpdateVariables { isPublished: boolean; name?: string | null; basePrice?: any | null; + seo?: SeoInput | null; } diff --git a/src/products/types/ProductVariant.ts b/src/products/types/ProductVariant.ts index 86239a599..bcc912d50 100644 --- a/src/products/types/ProductVariant.ts +++ b/src/products/types/ProductVariant.ts @@ -18,6 +18,7 @@ export interface ProductVariant_attributes_attribute { id: string; name: string | null; slug: string | null; + valueRequired: boolean; values: (ProductVariant_attributes_attribute_values | null)[] | null; } @@ -31,7 +32,7 @@ export interface ProductVariant_attributes_value { export interface ProductVariant_attributes { __typename: "SelectedAttribute"; attribute: ProductVariant_attributes_attribute; - value: ProductVariant_attributes_value; + value: ProductVariant_attributes_value | null; } export interface ProductVariant_costPrice { diff --git a/src/products/types/ProductVariantCreateData.ts b/src/products/types/ProductVariantCreateData.ts index 956873a6a..248ec0fe1 100644 --- a/src/products/types/ProductVariantCreateData.ts +++ b/src/products/types/ProductVariantCreateData.ts @@ -16,7 +16,6 @@ export interface ProductVariantCreateData_product_images { export interface ProductVariantCreateData_product_productType_variantAttributes_values { __typename: "AttributeValue"; id: string; - sortOrder: number | null; name: string | null; slug: string | null; } @@ -26,6 +25,7 @@ export interface ProductVariantCreateData_product_productType_variantAttributes id: string; slug: string | null; name: string | null; + valueRequired: boolean; values: (ProductVariantCreateData_product_productType_variantAttributes_values | null)[] | null; } diff --git a/src/products/types/ProductVariantDetails.ts b/src/products/types/ProductVariantDetails.ts index d7337cbac..116136551 100644 --- a/src/products/types/ProductVariantDetails.ts +++ b/src/products/types/ProductVariantDetails.ts @@ -18,6 +18,7 @@ export interface ProductVariantDetails_productVariant_attributes_attribute { id: string; name: string | null; slug: string | null; + valueRequired: boolean; values: (ProductVariantDetails_productVariant_attributes_attribute_values | null)[] | null; } @@ -31,7 +32,7 @@ export interface ProductVariantDetails_productVariant_attributes_value { export interface ProductVariantDetails_productVariant_attributes { __typename: "SelectedAttribute"; attribute: ProductVariantDetails_productVariant_attributes_attribute; - value: ProductVariantDetails_productVariant_attributes_value; + value: ProductVariantDetails_productVariant_attributes_value | null; } export interface ProductVariantDetails_productVariant_costPrice { diff --git a/src/products/types/SimpleProductUpdate.ts b/src/products/types/SimpleProductUpdate.ts index 66563c9aa..d958ad3c3 100644 --- a/src/products/types/SimpleProductUpdate.ts +++ b/src/products/types/SimpleProductUpdate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeValueInput, ProductVariantInput } from "./../../types/globalTypes"; +import { AttributeValueInput, ProductVariantInput, SeoInput, AttributeInputTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: SimpleProductUpdate @@ -58,6 +58,7 @@ export interface SimpleProductUpdate_productUpdate_product_purchaseCost { export interface SimpleProductUpdate_productUpdate_product_attributes_attribute_values { __typename: "AttributeValue"; + id: string; name: string | null; slug: string | null; } @@ -67,10 +68,12 @@ export interface SimpleProductUpdate_productUpdate_product_attributes_attribute id: string; slug: string | null; name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; values: (SimpleProductUpdate_productUpdate_product_attributes_attribute_values | null)[] | null; } -export interface SimpleProductUpdate_productUpdate_product_attributes_value { +export interface SimpleProductUpdate_productUpdate_product_attributes_values { __typename: "AttributeValue"; id: string; name: string | null; @@ -80,7 +83,7 @@ export interface SimpleProductUpdate_productUpdate_product_attributes_value { export interface SimpleProductUpdate_productUpdate_product_attributes { __typename: "SelectedAttribute"; attribute: SimpleProductUpdate_productUpdate_product_attributes_attribute; - value: SimpleProductUpdate_productUpdate_product_attributes_value; + values: (SimpleProductUpdate_productUpdate_product_attributes_values | null)[]; } export interface SimpleProductUpdate_productUpdate_product_pricing_priceRange_start_net { @@ -197,6 +200,7 @@ export interface SimpleProductUpdate_productVariantUpdate_productVariant_attribu id: string; name: string | null; slug: string | null; + valueRequired: boolean; values: (SimpleProductUpdate_productVariantUpdate_productVariant_attributes_attribute_values | null)[] | null; } @@ -210,7 +214,7 @@ export interface SimpleProductUpdate_productVariantUpdate_productVariant_attribu export interface SimpleProductUpdate_productVariantUpdate_productVariant_attributes { __typename: "SelectedAttribute"; attribute: SimpleProductUpdate_productVariantUpdate_productVariant_attributes_attribute; - value: SimpleProductUpdate_productVariantUpdate_productVariant_attributes_value; + value: SimpleProductUpdate_productVariantUpdate_productVariant_attributes_value | null; } export interface SimpleProductUpdate_productVariantUpdate_productVariant_costPrice { @@ -305,4 +309,5 @@ export interface SimpleProductUpdateVariables { basePrice?: any | null; productVariantId: string; productVariantInput: ProductVariantInput; + seo?: SeoInput | null; } diff --git a/src/products/types/VariantCreate.ts b/src/products/types/VariantCreate.ts index 3c8d9f64c..4dcfdd5d1 100644 --- a/src/products/types/VariantCreate.ts +++ b/src/products/types/VariantCreate.ts @@ -26,6 +26,7 @@ export interface VariantCreate_productVariantCreate_productVariant_attributes_at id: string; name: string | null; slug: string | null; + valueRequired: boolean; values: (VariantCreate_productVariantCreate_productVariant_attributes_attribute_values | null)[] | null; } @@ -39,7 +40,7 @@ export interface VariantCreate_productVariantCreate_productVariant_attributes_va export interface VariantCreate_productVariantCreate_productVariant_attributes { __typename: "SelectedAttribute"; attribute: VariantCreate_productVariantCreate_productVariant_attributes_attribute; - value: VariantCreate_productVariantCreate_productVariant_attributes_value; + value: VariantCreate_productVariantCreate_productVariant_attributes_value | null; } export interface VariantCreate_productVariantCreate_productVariant_costPrice { diff --git a/src/products/types/VariantImageAssign.ts b/src/products/types/VariantImageAssign.ts index dfc6bf05c..b3051bcbb 100644 --- a/src/products/types/VariantImageAssign.ts +++ b/src/products/types/VariantImageAssign.ts @@ -24,6 +24,7 @@ export interface VariantImageAssign_variantImageAssign_productVariant_attributes id: string; name: string | null; slug: string | null; + valueRequired: boolean; values: (VariantImageAssign_variantImageAssign_productVariant_attributes_attribute_values | null)[] | null; } @@ -37,7 +38,7 @@ export interface VariantImageAssign_variantImageAssign_productVariant_attributes export interface VariantImageAssign_variantImageAssign_productVariant_attributes { __typename: "SelectedAttribute"; attribute: VariantImageAssign_variantImageAssign_productVariant_attributes_attribute; - value: VariantImageAssign_variantImageAssign_productVariant_attributes_value; + value: VariantImageAssign_variantImageAssign_productVariant_attributes_value | null; } export interface VariantImageAssign_variantImageAssign_productVariant_costPrice { diff --git a/src/products/types/VariantImageUnassign.ts b/src/products/types/VariantImageUnassign.ts index 420d03223..56a791674 100644 --- a/src/products/types/VariantImageUnassign.ts +++ b/src/products/types/VariantImageUnassign.ts @@ -24,6 +24,7 @@ export interface VariantImageUnassign_variantImageUnassign_productVariant_attrib id: string; name: string | null; slug: string | null; + valueRequired: boolean; values: (VariantImageUnassign_variantImageUnassign_productVariant_attributes_attribute_values | null)[] | null; } @@ -37,7 +38,7 @@ export interface VariantImageUnassign_variantImageUnassign_productVariant_attrib export interface VariantImageUnassign_variantImageUnassign_productVariant_attributes { __typename: "SelectedAttribute"; attribute: VariantImageUnassign_variantImageUnassign_productVariant_attributes_attribute; - value: VariantImageUnassign_variantImageUnassign_productVariant_attributes_value; + value: VariantImageUnassign_variantImageUnassign_productVariant_attributes_value | null; } export interface VariantImageUnassign_variantImageUnassign_productVariant_costPrice { diff --git a/src/products/types/VariantUpdate.ts b/src/products/types/VariantUpdate.ts index e979e4edf..f58467f0d 100644 --- a/src/products/types/VariantUpdate.ts +++ b/src/products/types/VariantUpdate.ts @@ -26,6 +26,7 @@ export interface VariantUpdate_productVariantUpdate_productVariant_attributes_at id: string; name: string | null; slug: string | null; + valueRequired: boolean; values: (VariantUpdate_productVariantUpdate_productVariant_attributes_attribute_values | null)[] | null; } @@ -39,7 +40,7 @@ export interface VariantUpdate_productVariantUpdate_productVariant_attributes_va export interface VariantUpdate_productVariantUpdate_productVariant_attributes { __typename: "SelectedAttribute"; attribute: VariantUpdate_productVariantUpdate_productVariant_attributes_attribute; - value: VariantUpdate_productVariantUpdate_productVariant_attributes_value; + value: VariantUpdate_productVariantUpdate_productVariant_attributes_value | null; } export interface VariantUpdate_productVariantUpdate_productVariant_costPrice { diff --git a/src/products/views/ProductCreate.tsx b/src/products/views/ProductCreate.tsx index 78db87e84..47b70babe 100644 --- a/src/products/views/ProductCreate.tsx +++ b/src/products/views/ProductCreate.tsx @@ -9,7 +9,9 @@ import SearchCategories from "../../containers/SearchCategories"; import SearchCollections from "../../containers/SearchCollections"; import i18n from "../../i18n"; import { decimal, getMutationState, maybe } from "../../misc"; -import ProductCreatePage, { FormData } from "../components/ProductCreatePage"; +import ProductCreatePage, { + ProductCreatePageSubmitData +} from "../components/ProductCreatePage"; import { TypedProductCreateMutation } from "../mutations"; import { TypedProductCreateQuery } from "../queries"; import { ProductCreate } from "../types/ProductCreate"; @@ -42,6 +44,13 @@ export const ProductUpdate: React.StatelessComponent< text: i18n.t("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 }); + } } }; @@ -55,26 +64,33 @@ export const ProductUpdate: React.StatelessComponent< loading: productCreateDataLoading } ) => { - const handleSubmit = (formData: FormData) => { + const handleSubmit = ( + formData: ProductCreatePageSubmitData + ) => { productCreate({ variables: { - attributes: formData.attributes, + attributes: formData.attributes.map(attribute => ({ + id: attribute.id, + values: attribute.value + })), basePrice: decimal(formData.basePrice), - category: formData.category.value, + category: formData.category, chargeTaxes: formData.chargeTaxes, - collections: formData.collections.map( - collection => collection.value - ), + collections: formData.collections, descriptionJson: JSON.stringify( formData.description ), isPublished: formData.isPublished, name: formData.name, - productType: formData.productType.value.id, + productType: formData.productType, publicationDate: formData.publicationDate !== "" ? formData.publicationDate : null, + seo: { + description: formData.seoDescription, + title: formData.seoTitle + }, sku: formData.sku, stockQuantity: formData.stockQuantity !== null diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index 7494e6053..17f6d7e00 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -9,7 +9,9 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; import SaveFilterTabDialog, { SaveFilterTabDialogFormData } from "@saleor/components/SaveFilterTabDialog"; +import { defaultListSettings, ProductListColumns } from "@saleor/config"; import useBulkActions from "@saleor/hooks/useBulkActions"; +import useListSettings from "@saleor/hooks/useListSettings"; import useLocale from "@saleor/hooks/useLocale"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; @@ -17,10 +19,10 @@ import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; import useShop from "@saleor/hooks/useShop"; -import { PAGINATE_BY } from "../../../config"; -import i18n from "../../../i18n"; -import { getMutationState, maybe } from "../../../misc"; -import ProductListCard from "../../components/ProductListCard"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; +import ProductListPage from "../../components/ProductListPage"; import { TypedProductBulkDeleteMutation, TypedProductBulkPublishMutation @@ -62,7 +64,9 @@ export const ProductList: React.StatelessComponent = ({ const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); - + const { updateListSettings, settings } = useListSettings( + ListViews.PRODUCT_LIST + ); const tabs = getFilterTabs(); const currentTab = @@ -128,14 +132,14 @@ export const ProductList: React.StatelessComponent = ({ handleTabChange(tabs.length + 1); }; - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); const currencySymbol = maybe(() => shop.defaultCurrency, "USD"); const queryVariables = React.useMemo( () => ({ ...paginationState, filter: getFilterVariables(params) }), - [params] + [params, settings.rowNumber] ); return ( @@ -193,9 +197,13 @@ export const ProductList: React.StatelessComponent = ({ return ( <> - = ({ )} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} pageInfo={pageInfo} onRowClick={id => () => navigate(productUrl(id))} onAll={() => @@ -261,7 +270,9 @@ export const ProductList: React.StatelessComponent = ({ confirmButtonState={bulkDeleteMutationState} onClose={closeModal} onConfirm={() => - productBulkDelete({ variables: { ids: params.ids } }) + productBulkDelete({ + variables: { ids: params.ids } + }) } title={i18n.t("Remove products")} variant="delete" diff --git a/src/products/views/ProductUpdate.tsx b/src/products/views/ProductUpdate.tsx index c67671c79..9080c2738 100644 --- a/src/products/views/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate.tsx @@ -4,7 +4,7 @@ import DeleteIcon from "@material-ui/icons/Delete"; import React from "react"; import { arrayMove } from "react-sortable-hoc"; -import * as placeholderImg from "@assets/images/placeholder255x255.png"; +import placeholderImg from "@assets/images/placeholder255x255.png"; import ActionDialog from "@saleor/components/ActionDialog"; import { WindowTitle } from "@saleor/components/WindowTitle"; import useBulkActions from "@saleor/hooks/useBulkActions"; diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 846a85654..cd6919a50 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -8,7 +8,7 @@ import { WindowTitle } from "@saleor/components/WindowTitle"; import useBulkActions from "@saleor/hooks/useBulkActions"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; -import placeholderImg from "../../../../images/placeholder255x255.png"; +import placeholderImg from "../@assets/images/placeholder255x255.png"; import { DEFAULT_INITIAL_SEARCH_DATA } from "../../../config"; import SearchCategories from "../../../containers/SearchCategories"; import SearchCollections from "../../../containers/SearchCollections"; diff --git a/src/products/views/ProductVariant.tsx b/src/products/views/ProductVariant.tsx index 6af9aa183..45a95b4e9 100644 --- a/src/products/views/ProductVariant.tsx +++ b/src/products/views/ProductVariant.tsx @@ -1,13 +1,15 @@ import React from "react"; -import * as placeholderImg from "@assets/images/placeholder255x255.png"; import { WindowTitle } from "@saleor/components/WindowTitle"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; +import placeholderImg from "@assets/images/placeholder255x255.png"; import i18n from "../../i18n"; import { decimal, getMutationState, maybe } from "../../misc"; import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog"; -import ProductVariantPage from "../components/ProductVariantPage"; +import ProductVariantPage, { + ProductVariantPageSubmitData +} from "../components/ProductVariantPage"; import ProductVariantOperations from "../containers/ProductVariantOperations"; import { TypedProductVariantQuery } from "../queries"; import { VariantUpdate } from "../types/VariantUpdate"; @@ -24,18 +26,6 @@ interface ProductUpdateProps { params: ProductVariantEditUrlQueryParams; } -interface FormData { - id: string; - attributes?: Array<{ - slug: string; - value: string; - }>; - costPrice?: string; - priceOverride?: string; - quantity: number; - sku: string; -} - export const ProductVariant: React.StatelessComponent = ({ variantId, productId, @@ -127,10 +117,13 @@ export const ProductVariant: React.StatelessComponent = ({ ) } onImageSelect={handleImageSelect} - onSubmit={(data: FormData) => { + onSubmit={(data: ProductVariantPageSubmitData) => { if (variant) { updateVariant.mutate({ - attributes: data.attributes ? data.attributes : null, + attributes: data.attributes.map(attribute => ({ + id: attribute.id, + values: [attribute.value] + })), costPrice: decimal(data.costPrice), id: variantId, priceOverride: decimal(data.priceOverride), diff --git a/src/products/views/ProductVariantCreate.tsx b/src/products/views/ProductVariantCreate.tsx index 848f1480e..ede273a8e 100644 --- a/src/products/views/ProductVariantCreate.tsx +++ b/src/products/views/ProductVariantCreate.tsx @@ -6,7 +6,9 @@ import useNotifier from "@saleor/hooks/useNotifier"; import useShop from "@saleor/hooks/useShop"; import i18n from "../../i18n"; import { decimal, getMutationState, maybe } from "../../misc"; -import ProductVariantCreatePage from "../components/ProductVariantCreatePage"; +import ProductVariantCreatePage, { + ProductVariantCreatePageSubmitData +} from "../components/ProductVariantCreatePage"; import { TypedVariantCreateMutation } from "../mutations"; import { TypedProductVariantCreateQuery } from "../queries"; import { VariantCreate } from "../types/VariantCreate"; @@ -16,17 +18,6 @@ interface ProductUpdateProps { productId: string; } -interface FormData { - attributes?: Array<{ - slug: string; - value: string; - }>; - costPrice?: string; - priceOverride?: string; - quantity: number; - sku: string; -} - export const ProductVariant: React.StatelessComponent = ({ productId }) => { @@ -57,10 +48,17 @@ export const ProductVariant: React.StatelessComponent = ({ {(variantCreate, variantCreateResult) => { const handleBack = () => navigate(productUrl(productId)); - const handleSubmit = (formData: FormData) => + const handleSubmit = ( + formData: ProductVariantCreatePageSubmitData + ) => variantCreate({ variables: { - attributes: formData.attributes, + attributes: formData.attributes + .filter(attribute => attribute.value !== "") + .map(attribute => ({ + id: attribute.id, + values: [attribute.value] + })), costPrice: decimal(formData.costPrice), priceOverride: decimal(formData.priceOverride), product: productId, diff --git a/src/shipping/components/ShippingZonesList/ShippingZonesList.tsx b/src/shipping/components/ShippingZonesList/ShippingZonesList.tsx index 838fe4f14..f38a21ab6 100644 --- a/src/shipping/components/ShippingZonesList/ShippingZonesList.tsx +++ b/src/shipping/components/ShippingZonesList/ShippingZonesList.tsx @@ -20,10 +20,10 @@ import Checkbox from "@saleor/components/Checkbox"; import Skeleton from "@saleor/components/Skeleton"; import TableHead from "@saleor/components/TableHead"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; -import { maybe, renderCollection } from "../../../misc"; -import { ICONBUTTON_SIZE } from "../../../theme"; -import { ListActions, ListProps } from "../../../types"; +import i18n from "@saleor/i18n"; +import { maybe, renderCollection } from "@saleor/misc"; +import { ICONBUTTON_SIZE } from "@saleor/theme"; +import { ListActions, ListProps } from "@saleor/types"; import { ShippingZoneFragment } from "../../types/ShippingZoneFragment"; export interface ShippingZonesListProps extends ListProps, ListActions { @@ -50,14 +50,19 @@ const styles = (theme: Theme) => cursor: "pointer" } }); + +const numberOfColumns = 4; + const ShippingZonesList = withStyles(styles, { name: "ShippingZonesList" })( ({ classes, disabled, + settings, onAdd, onNextPage, onPreviousPage, onRemove, + onUpdateListSettings, onRowClick, pageInfo, shippingZones, @@ -81,6 +86,7 @@ const ShippingZonesList = withStyles(styles, { name: "ShippingZonesList" })( />
{i18n.t("Countries", { context: "object" })} + toggle(shippingZone.id)} /> @@ -159,7 +169,7 @@ const ShippingZonesList = withStyles(styles, { name: "ShippingZonesList" })( }, () => ( - + {i18n.t("No shipping zones found")} diff --git a/src/shipping/components/ShippingZonesListPage/ShippingZonesListPage.tsx b/src/shipping/components/ShippingZonesListPage/ShippingZonesListPage.tsx index f63fddfe1..b9aa74b37 100644 --- a/src/shipping/components/ShippingZonesListPage/ShippingZonesListPage.tsx +++ b/src/shipping/components/ShippingZonesListPage/ShippingZonesListPage.tsx @@ -4,9 +4,9 @@ import AppHeader from "@saleor/components/AppHeader"; import Container from "@saleor/components/Container"; import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; -import i18n from "../../../i18n"; -import { ListActions, PageListProps } from "../../../types"; -import { WeightUnitsEnum } from "../../../types/globalTypes"; +import i18n from "@saleor/i18n"; +import { ListActions, PageListProps } from "@saleor/types"; +import { WeightUnitsEnum } from "@saleor/types/globalTypes"; import { ShippingZoneFragment } from "../../types/ShippingZoneFragment"; import ShippingWeightUnitForm from "../ShippingWeightUnitForm"; import ShippingZonesList from "../ShippingZonesList"; diff --git a/src/shipping/views/ShippingZonesList.tsx b/src/shipping/views/ShippingZonesList.tsx index 6559a4fa9..501ec532a 100644 --- a/src/shipping/views/ShippingZonesList.tsx +++ b/src/shipping/views/ShippingZonesList.tsx @@ -4,17 +4,18 @@ import DeleteIcon from "@material-ui/icons/Delete"; import React from "react"; import ActionDialog from "@saleor/components/ActionDialog"; +import { configurationMenuUrl } from "@saleor/configuration"; import useBulkActions from "@saleor/hooks/useBulkActions"; +import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; import useShop from "@saleor/hooks/useShop"; -import { PAGINATE_BY } from "../../config"; -import { configurationMenuUrl } from "../../configuration"; -import i18n from "../../i18n"; -import { getMutationState, maybe } from "../../misc"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; import ShippingZonesListPage from "../components/ShippingZonesListPage"; import { TypedBulkDeleteShippingZone, @@ -46,8 +47,11 @@ export const ShippingZonesList: React.StatelessComponent< const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( params.ids ); + const { updateListSettings, settings } = useListSettings( + ListViews.SHIPPING_METHODS_LIST + ); - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); return ( @@ -145,6 +149,7 @@ export const ShippingZonesList: React.StatelessComponent< defaultWeightUnit={maybe( () => shop.defaultWeightUnit )} + settings={settings} disabled={ loading || deleteShippingZoneOpts.loading || @@ -156,6 +161,7 @@ export const ShippingZonesList: React.StatelessComponent< pageInfo={pageInfo} onAdd={() => navigate(shippingZoneAddUrl)} onBack={() => navigate(configurationMenuUrl)} + onUpdateListSettings={updateListSettings} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} onRemove={id => diff --git a/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx b/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx index 5cfa4c6f3..794b7ac1a 100644 --- a/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx +++ b/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx @@ -8,14 +8,31 @@ import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import SaveButtonBar from "@saleor/components/SaveButtonBar"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { UserError } from "@saleor/types"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; +import { mapCountriesToChoices } from "@saleor/utils/maps"; import i18n from "../../../i18n"; import { maybe } from "../../../misc"; import { AuthorizationKeyType } from "../../../types/globalTypes"; import { SiteSettings_shop } from "../../types/SiteSettings"; +import SiteSettingsAddress from "../SiteSettingsAddress/SiteSettingsAddress"; import SiteSettingsDetails from "../SiteSettingsDetails/SiteSettingsDetails"; import SiteSettingsKeys from "../SiteSettingsKeys/SiteSettingsKeys"; -export interface SiteSettingsPageFormData { +export interface SiteSettingsPageAddressFormData { + city: string; + companyName: string; + country: string; + countryArea: string; + phone: string; + postalCode: string; + streetAddress1: string; + streetAddress2: string; +} + +export interface SiteSettingsPageFormData + extends SiteSettingsPageAddressFormData { description: string; domain: string; name: string; @@ -23,10 +40,7 @@ export interface SiteSettingsPageFormData { export interface SiteSettingsPageProps { disabled: boolean; - errors: Array<{ - field: string; - message: string; - }>; + errors: UserError[]; shop: SiteSettings_shop; saveButtonBarState: ConfirmButtonTransitionState; onBack: () => void; @@ -45,11 +59,24 @@ const SiteSettingsPage: React.StatelessComponent = ({ onKeyRemove, onSubmit }) => { + const [displayCountry, setDisplayCountry] = useStateFromProps( + maybe(() => shop.companyAddress.country.code, "") + ); + const initialForm: SiteSettingsPageFormData = { + city: maybe(() => shop.companyAddress.city, ""), + companyName: maybe(() => shop.companyAddress.companyName, ""), + country: maybe(() => shop.companyAddress.country.code, ""), + countryArea: maybe(() => shop.companyAddress.countryArea, ""), description: maybe(() => shop.description, ""), domain: maybe(() => shop.domain.host, ""), - name: maybe(() => shop.name, "") + name: maybe(() => shop.name, ""), + phone: maybe(() => shop.companyAddress.phone, ""), + postalCode: maybe(() => shop.companyAddress.postalCode, ""), + streetAddress1: maybe(() => shop.companyAddress.streetAddress1, ""), + streetAddress2: maybe(() => shop.companyAddress.streetAddress2, "") }; + return (
= ({ onSubmit={onSubmit} confirmLeave > - {({ change, data, errors: formErrors, hasChanged, submit }) => ( - - {i18n.t("Configuration")} - - - {i18n.t("Site Settings")} - { + const countryChoices = mapCountriesToChoices( + maybe(() => shop.countries, []) + ); + const handleCountryChange = createSingleAutocompleteSelectHandler( + change, + setDisplayCountry, + countryChoices + ); + + return ( + + {i18n.t("Configuration")} + - - {i18n.t("Authentication keys")} - - shop.authorizationKeys)} - onAdd={onKeyAdd} - onRemove={onKeyRemove} + + {i18n.t("Site Settings")} + + + {i18n.t("Company information")} + + + + {i18n.t("Authentication keys")} + + shop.authorizationKeys)} + onAdd={onKeyAdd} + onRemove={onKeyRemove} + /> + + - - - - )} + + ); + }} ); }; diff --git a/src/siteSettings/fixtures.ts b/src/siteSettings/fixtures.ts index b41da4c79..59e41ca41 100644 --- a/src/siteSettings/fixtures.ts +++ b/src/siteSettings/fixtures.ts @@ -10,6 +10,32 @@ export const shop: SiteSettings_shop = { name: AuthorizationKeyType.FACEBOOK } ], + companyAddress: { + __typename: "Address", + city: "Kenstad", + cityArea: "Alabama", + companyName: "Saleor e-commerce", + country: { + __typename: "CountryDisplay", + code: "UA", + country: "United Arab Emirates" + }, + countryArea: null, + firstName: null, + id: "1", + lastName: null, + phone: "+41 876-373-9137", + postalCode: "89880-6342", + streetAddress1: "01419 Bernhard Plain", + streetAddress2: null + }, + countries: [ + { + __typename: "CountryDisplay", + code: "UA", + country: "United Arab Emirates" + } + ], description: "Lorem ipsum dolor sit amet", domain: { __typename: "Domain", diff --git a/src/siteSettings/mutations.ts b/src/siteSettings/mutations.ts index 31b531d85..048217b7a 100644 --- a/src/siteSettings/mutations.ts +++ b/src/siteSettings/mutations.ts @@ -1,6 +1,7 @@ import gql from "graphql-tag"; import { TypedMutation } from "../mutations"; +import { fragmentAddress } from "../orders/queries"; import { shopFragment } from "./queries"; import { AuthorizationKeyAdd, @@ -58,9 +59,11 @@ export const TypedAuthorizationKeyDelete = TypedMutation< const shopSettingsUpdate = gql` ${shopFragment} + ${fragmentAddress} mutation ShopSettingsUpdate( $shopDomainInput: SiteDomainInput! $shopSettingsInput: ShopSettingsInput! + $addressInput: AddressInput! ) { shopSettingsUpdate(input: $shopSettingsInput) { errors { @@ -83,6 +86,17 @@ const shopSettingsUpdate = gql` } } } + shopAddressUpdate(input: $addressInput) { + errors { + field + message + } + shop { + companyAddress { + ...AddressFragment + } + } + } } `; export const TypedShopSettingsUpdate = TypedMutation< diff --git a/src/siteSettings/queries.ts b/src/siteSettings/queries.ts index 07128b873..b6a57c61a 100644 --- a/src/siteSettings/queries.ts +++ b/src/siteSettings/queries.ts @@ -1,13 +1,22 @@ import gql from "graphql-tag"; +import { fragmentAddress } from "../orders/queries"; import { TypedQuery } from "../queries"; import { SiteSettings } from "./types/SiteSettings"; export const shopFragment = gql` + ${fragmentAddress} fragment ShopFragment on Shop { authorizationKeys { key name } + companyAddress { + ...AddressFragment + } + countries { + code + country + } description domain { host diff --git a/src/siteSettings/types/AuthorizationKeyAdd.ts b/src/siteSettings/types/AuthorizationKeyAdd.ts index c6524fdbf..a326a9cca 100644 --- a/src/siteSettings/types/AuthorizationKeyAdd.ts +++ b/src/siteSettings/types/AuthorizationKeyAdd.ts @@ -20,6 +20,34 @@ export interface AuthorizationKeyAdd_authorizationKeyAdd_shop_authorizationKeys name: AuthorizationKeyType; } +export interface AuthorizationKeyAdd_authorizationKeyAdd_shop_companyAddress_country { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface AuthorizationKeyAdd_authorizationKeyAdd_shop_companyAddress { + __typename: "Address"; + city: string; + cityArea: string; + companyName: string; + country: AuthorizationKeyAdd_authorizationKeyAdd_shop_companyAddress_country; + countryArea: string; + firstName: string; + id: string; + lastName: string; + phone: string | null; + postalCode: string; + streetAddress1: string; + streetAddress2: string; +} + +export interface AuthorizationKeyAdd_authorizationKeyAdd_shop_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + export interface AuthorizationKeyAdd_authorizationKeyAdd_shop_domain { __typename: "Domain"; host: string; @@ -28,6 +56,8 @@ export interface AuthorizationKeyAdd_authorizationKeyAdd_shop_domain { export interface AuthorizationKeyAdd_authorizationKeyAdd_shop { __typename: "Shop"; authorizationKeys: (AuthorizationKeyAdd_authorizationKeyAdd_shop_authorizationKeys | null)[]; + companyAddress: AuthorizationKeyAdd_authorizationKeyAdd_shop_companyAddress | null; + countries: (AuthorizationKeyAdd_authorizationKeyAdd_shop_countries | null)[]; description: string | null; domain: AuthorizationKeyAdd_authorizationKeyAdd_shop_domain; name: string; diff --git a/src/siteSettings/types/AuthorizationKeyDelete.ts b/src/siteSettings/types/AuthorizationKeyDelete.ts index a79185000..024e7f599 100644 --- a/src/siteSettings/types/AuthorizationKeyDelete.ts +++ b/src/siteSettings/types/AuthorizationKeyDelete.ts @@ -20,6 +20,34 @@ export interface AuthorizationKeyDelete_authorizationKeyDelete_shop_authorizatio name: AuthorizationKeyType; } +export interface AuthorizationKeyDelete_authorizationKeyDelete_shop_companyAddress_country { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface AuthorizationKeyDelete_authorizationKeyDelete_shop_companyAddress { + __typename: "Address"; + city: string; + cityArea: string; + companyName: string; + country: AuthorizationKeyDelete_authorizationKeyDelete_shop_companyAddress_country; + countryArea: string; + firstName: string; + id: string; + lastName: string; + phone: string | null; + postalCode: string; + streetAddress1: string; + streetAddress2: string; +} + +export interface AuthorizationKeyDelete_authorizationKeyDelete_shop_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + export interface AuthorizationKeyDelete_authorizationKeyDelete_shop_domain { __typename: "Domain"; host: string; @@ -28,6 +56,8 @@ export interface AuthorizationKeyDelete_authorizationKeyDelete_shop_domain { export interface AuthorizationKeyDelete_authorizationKeyDelete_shop { __typename: "Shop"; authorizationKeys: (AuthorizationKeyDelete_authorizationKeyDelete_shop_authorizationKeys | null)[]; + companyAddress: AuthorizationKeyDelete_authorizationKeyDelete_shop_companyAddress | null; + countries: (AuthorizationKeyDelete_authorizationKeyDelete_shop_countries | null)[]; description: string | null; domain: AuthorizationKeyDelete_authorizationKeyDelete_shop_domain; name: string; diff --git a/src/siteSettings/types/ShopFragment.ts b/src/siteSettings/types/ShopFragment.ts index dcea4c527..d27566ab8 100644 --- a/src/siteSettings/types/ShopFragment.ts +++ b/src/siteSettings/types/ShopFragment.ts @@ -14,6 +14,34 @@ export interface ShopFragment_authorizationKeys { name: AuthorizationKeyType; } +export interface ShopFragment_companyAddress_country { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface ShopFragment_companyAddress { + __typename: "Address"; + city: string; + cityArea: string; + companyName: string; + country: ShopFragment_companyAddress_country; + countryArea: string; + firstName: string; + id: string; + lastName: string; + phone: string | null; + postalCode: string; + streetAddress1: string; + streetAddress2: string; +} + +export interface ShopFragment_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + export interface ShopFragment_domain { __typename: "Domain"; host: string; @@ -22,6 +50,8 @@ export interface ShopFragment_domain { export interface ShopFragment { __typename: "Shop"; authorizationKeys: (ShopFragment_authorizationKeys | null)[]; + companyAddress: ShopFragment_companyAddress | null; + countries: (ShopFragment_countries | null)[]; description: string | null; domain: ShopFragment_domain; name: string; diff --git a/src/siteSettings/types/ShopSettingsUpdate.ts b/src/siteSettings/types/ShopSettingsUpdate.ts index e78f9c121..2fa80ac0d 100644 --- a/src/siteSettings/types/ShopSettingsUpdate.ts +++ b/src/siteSettings/types/ShopSettingsUpdate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { SiteDomainInput, ShopSettingsInput, AuthorizationKeyType } from "./../../types/globalTypes"; +import { SiteDomainInput, ShopSettingsInput, AddressInput, AuthorizationKeyType } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: ShopSettingsUpdate @@ -20,6 +20,34 @@ export interface ShopSettingsUpdate_shopSettingsUpdate_shop_authorizationKeys { name: AuthorizationKeyType; } +export interface ShopSettingsUpdate_shopSettingsUpdate_shop_companyAddress_country { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface ShopSettingsUpdate_shopSettingsUpdate_shop_companyAddress { + __typename: "Address"; + city: string; + cityArea: string; + companyName: string; + country: ShopSettingsUpdate_shopSettingsUpdate_shop_companyAddress_country; + countryArea: string; + firstName: string; + id: string; + lastName: string; + phone: string | null; + postalCode: string; + streetAddress1: string; + streetAddress2: string; +} + +export interface ShopSettingsUpdate_shopSettingsUpdate_shop_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + export interface ShopSettingsUpdate_shopSettingsUpdate_shop_domain { __typename: "Domain"; host: string; @@ -28,6 +56,8 @@ export interface ShopSettingsUpdate_shopSettingsUpdate_shop_domain { export interface ShopSettingsUpdate_shopSettingsUpdate_shop { __typename: "Shop"; authorizationKeys: (ShopSettingsUpdate_shopSettingsUpdate_shop_authorizationKeys | null)[]; + companyAddress: ShopSettingsUpdate_shopSettingsUpdate_shop_companyAddress | null; + countries: (ShopSettingsUpdate_shopSettingsUpdate_shop_countries | null)[]; description: string | null; domain: ShopSettingsUpdate_shopSettingsUpdate_shop_domain; name: string; @@ -62,12 +92,53 @@ export interface ShopSettingsUpdate_shopDomainUpdate { shop: ShopSettingsUpdate_shopDomainUpdate_shop | null; } +export interface ShopSettingsUpdate_shopAddressUpdate_errors { + __typename: "Error"; + field: string | null; + message: string | null; +} + +export interface ShopSettingsUpdate_shopAddressUpdate_shop_companyAddress_country { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface ShopSettingsUpdate_shopAddressUpdate_shop_companyAddress { + __typename: "Address"; + city: string; + cityArea: string; + companyName: string; + country: ShopSettingsUpdate_shopAddressUpdate_shop_companyAddress_country; + countryArea: string; + firstName: string; + id: string; + lastName: string; + phone: string | null; + postalCode: string; + streetAddress1: string; + streetAddress2: string; +} + +export interface ShopSettingsUpdate_shopAddressUpdate_shop { + __typename: "Shop"; + companyAddress: ShopSettingsUpdate_shopAddressUpdate_shop_companyAddress | null; +} + +export interface ShopSettingsUpdate_shopAddressUpdate { + __typename: "ShopAddressUpdate"; + errors: ShopSettingsUpdate_shopAddressUpdate_errors[] | null; + shop: ShopSettingsUpdate_shopAddressUpdate_shop | null; +} + export interface ShopSettingsUpdate { shopSettingsUpdate: ShopSettingsUpdate_shopSettingsUpdate | null; shopDomainUpdate: ShopSettingsUpdate_shopDomainUpdate | null; + shopAddressUpdate: ShopSettingsUpdate_shopAddressUpdate | null; } export interface ShopSettingsUpdateVariables { shopDomainInput: SiteDomainInput; shopSettingsInput: ShopSettingsInput; + addressInput: AddressInput; } diff --git a/src/siteSettings/types/SiteSettings.ts b/src/siteSettings/types/SiteSettings.ts index 873124078..ee049f738 100644 --- a/src/siteSettings/types/SiteSettings.ts +++ b/src/siteSettings/types/SiteSettings.ts @@ -14,6 +14,34 @@ export interface SiteSettings_shop_authorizationKeys { name: AuthorizationKeyType; } +export interface SiteSettings_shop_companyAddress_country { + __typename: "CountryDisplay"; + code: string; + country: string; +} + +export interface SiteSettings_shop_companyAddress { + __typename: "Address"; + city: string; + cityArea: string; + companyName: string; + country: SiteSettings_shop_companyAddress_country; + countryArea: string; + firstName: string; + id: string; + lastName: string; + phone: string | null; + postalCode: string; + streetAddress1: string; + streetAddress2: string; +} + +export interface SiteSettings_shop_countries { + __typename: "CountryDisplay"; + code: string; + country: string; +} + export interface SiteSettings_shop_domain { __typename: "Domain"; host: string; @@ -22,6 +50,8 @@ export interface SiteSettings_shop_domain { export interface SiteSettings_shop { __typename: "Shop"; authorizationKeys: (SiteSettings_shop_authorizationKeys | null)[]; + companyAddress: SiteSettings_shop_companyAddress | null; + countries: (SiteSettings_shop_countries | null)[]; description: string | null; domain: SiteSettings_shop_domain; name: string; diff --git a/src/siteSettings/views/index.tsx b/src/siteSettings/views/index.tsx index a1032f38e..bb593d907 100644 --- a/src/siteSettings/views/index.tsx +++ b/src/siteSettings/views/index.tsx @@ -65,7 +65,9 @@ export const SiteSettings: React.StatelessComponent = ({ (!data.shopDomainUpdate.errors || data.shopDomainUpdate.errors.length === 0) && (!data.shopSettingsUpdate.errors || - data.shopSettingsUpdate.errors.length === 0) + data.shopSettingsUpdate.errors.length === 0) && + (!data.shopAddressUpdate.errors || + data.shopAddressUpdate.errors.length === 0) ) { notify({ text: i18n.t("Site settings updated", { @@ -95,6 +97,11 @@ export const SiteSettings: React.StatelessComponent = ({ () => updateShopSettingsOpts.data.shopSettingsUpdate.errors, [] + ), + ...maybe( + () => + updateShopSettingsOpts.data.shopAddressUpdate.errors, + [] ) ]; const loading = @@ -119,6 +126,16 @@ export const SiteSettings: React.StatelessComponent = ({ ) => updateShopSettings({ variables: { + addressInput: { + city: data.city, + companyName: data.companyName, + country: data.country, + countryArea: data.countryArea, + phone: data.phone, + postalCode: data.postalCode, + streetAddress1: data.streetAddress1, + streetAddress2: data.streetAddress2 + }, shopDomainInput: { domain: data.domain, name: data.name diff --git a/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx b/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx index 7156935e3..a104b703b 100644 --- a/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx +++ b/src/staff/components/StaffDetailsPage/StaffDetailsPage.tsx @@ -58,8 +58,8 @@ const StaffDetailsPage: React.StatelessComponent = ({ onSubmit }: StaffDetailsPageProps) => { const initialForm: FormData = { - email: maybe(() => staffMember.email), - firstName: maybe(() => staffMember.firstName), + email: maybe(() => staffMember.email, ""), + firstName: maybe(() => staffMember.firstName, ""), hasFullAccess: maybe( () => permissions.filter( @@ -71,7 +71,7 @@ const StaffDetailsPage: React.StatelessComponent = ({ false ), isActive: maybe(() => staffMember.isActive, false), - lastName: maybe(() => staffMember.lastName), + lastName: maybe(() => staffMember.lastName, ""), permissions: maybe(() => staffMember.permissions, []).map(perm => perm.code) }; return ( diff --git a/src/staff/components/StaffList/StaffList.tsx b/src/staff/components/StaffList/StaffList.tsx index f15a69e6a..a3c67c56a 100644 --- a/src/staff/components/StaffList/StaffList.tsx +++ b/src/staff/components/StaffList/StaffList.tsx @@ -12,19 +12,19 @@ import TableFooter from "@material-ui/core/TableFooter"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import Typography from "@material-ui/core/Typography"; -import * as classNames from "classnames"; +import classNames from "classnames"; import React from "react"; import Skeleton from "@saleor/components/Skeleton"; import TablePagination from "@saleor/components/TablePagination"; -import i18n from "../../../i18n"; +import i18n from "@saleor/i18n"; import { getUserInitials, getUserName, maybe, renderCollection -} from "../../../misc"; -import { ListProps } from "../../../types"; +} from "@saleor/misc"; +import { ListProps } from "@saleor/types"; import { StaffList_staffUsers_edges_node } from "../../types/StaffList"; const styles = (theme: Theme) => @@ -72,9 +72,11 @@ interface StaffListProps extends ListProps, WithStyles { const StaffList = withStyles(styles, { name: "StaffList" })( ({ classes, + settings, disabled, onNextPage, onPreviousPage, + onUpdateListSettings, onRowClick, pageInfo, staffMembers @@ -95,10 +97,12 @@ const StaffList = withStyles(styles, { name: "StaffList" })( marginTop: theme.spacing.unit }, hr: { - backgroundColor: "#eaeaea", + backgroundColor: theme.overrides.MuiCard.root.borderColor, border: "none", height: 1, marginBottom: 0, diff --git a/src/staff/components/StaffProperties/StaffProperties.tsx b/src/staff/components/StaffProperties/StaffProperties.tsx index e4312e044..17a1a0517 100644 --- a/src/staff/components/StaffProperties/StaffProperties.tsx +++ b/src/staff/components/StaffProperties/StaffProperties.tsx @@ -16,7 +16,7 @@ import i18n from "../../../i18n"; import { getUserInitials, maybe } from "../../../misc"; import { StaffMemberDetails_user } from "../../types/StaffMemberDetails"; -import * as photoIcon from "@assets/images/photo-icon.svg"; +import photoIcon from "../@assets/images/photo-icon.svg"; const styles = (theme: Theme) => createStyles({ diff --git a/src/staff/views/StaffList.tsx b/src/staff/views/StaffList.tsx index eb3d94f50..a730d8695 100644 --- a/src/staff/views/StaffList.tsx +++ b/src/staff/views/StaffList.tsx @@ -1,14 +1,16 @@ import React from "react"; +import useListSettings from "@saleor/hooks/useListSettings"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import usePaginator, { createPaginationState } from "@saleor/hooks/usePaginator"; -import { PAGINATE_BY } from "../../config"; -import { configurationMenuUrl } from "../../configuration"; -import i18n from "../../i18n"; -import { getMutationState, maybe } from "../../misc"; + +import { configurationMenuUrl } from "@saleor/configuration"; +import i18n from "@saleor/i18n"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; import StaffAddMemberDialog, { FormData as AddStaffMemberForm } from "../components/StaffAddMemberDialog"; @@ -32,6 +34,9 @@ export const StaffList: React.StatelessComponent = ({ const navigate = useNavigator(); const notify = useNotifier(); const paginate = usePaginator(); + const { updateListSettings, settings } = useListSettings( + ListViews.STAFF_MEMBERS_LIST + ); const closeModal = () => navigate( @@ -43,7 +48,7 @@ export const StaffList: React.StatelessComponent = ({ true ); - const paginationState = createPaginationState(PAGINATE_BY, params); + const paginationState = createPaginationState(settings.rowNumber, params); return ( {({ data, loading }) => { @@ -91,6 +96,7 @@ export const StaffList: React.StatelessComponent = ({ <> data.staffUsers.edges.map(edge => edge.node) @@ -105,6 +111,7 @@ export const StaffList: React.StatelessComponent = ({ onBack={() => navigate(configurationMenuUrl)} onNextPage={loadNextPage} onPreviousPage={loadPreviousPage} + onUpdateListSettings={updateListSettings} onRowClick={id => () => navigate(staffMemberDetailsUrl(id))} /> ( - - - - - -
- {storyFn()} -
-
-
-
-
-
+ + + + +
+ {storyFn()} +
+
+
+
+
); export default Decorator; diff --git a/src/storybook/Stories.test.ts b/src/storybook/Stories.test.ts index e6f85b866..3cb047195 100644 --- a/src/storybook/Stories.test.ts +++ b/src/storybook/Stories.test.ts @@ -1,4 +1,5 @@ import createGenerateClassName from "@material-ui/core/styles/createGenerateClassName"; +import createHookGenerateClassName from "@material-ui/styles/createGenerateClassName"; import initStoryshots from "@storybook/addon-storyshots"; // tslint:disable no-submodule-imports import generateRandomKey from "draft-js/lib/generateRandomKey"; @@ -15,11 +16,18 @@ jest.mock("@material-ui/core/styles/createGenerateClassName"); } ); +jest.mock("@material-ui/styles/createGenerateClassName"); +(createHookGenerateClassName as any).mockImplementation( + () => (rule, stylesheet) => { + return [stylesheet.options.meta, rule.key, "id"].join("-"); + } +); + jest.mock("draft-js/lib/generateRandomKey"); (generateRandomKey as any).mockImplementation(() => "testKey"); initStoryshots({ - configPath: "src/storybook/", + configPath: "saleor/static/dashboard-next/storybook/", test({ story }) { const result = render(story.render() as any); expect(toJSON(result)).toMatchSnapshot(); diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 17bd0d0a3..f8eba83b1 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -1,886 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots Categories / CategoryProducts when loading data 1`] = ` +exports[`Storyshots Attributes / Attribute delete default 1`] = `
-
-
- - Products - -
- -
-
-
-
-
- - - - - - - - - - - - - - - - - - -
- - Name - - Type -
-
- -
-
- - ‌ - - - - ‌ - -
- - +/> `; -exports[`Storyshots Categories / CategoryProducts with clickable rows 1`] = ` +exports[`Storyshots Attributes / Attribute value delete default 1`] = `
-
-
- - Products - -
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Name - - Type -
-
- -
-
- - Gardner, Graham and King - - - T-Shirt -
-
- -
-
- - Gardner, Graham and King - - - T-Shirt -
-
- -
-
- - Gardner, Graham and King - - - T-Shirt -
-
- -
-
- - Gardner, Graham and King - - - T-Shirt -
-
-
+/> `; -exports[`Storyshots Categories / CategoryProducts with initial data 1`] = ` +exports[`Storyshots Attributes / Attribute value edit default 1`] = `
-
-
- - Products - -
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Name - - Type -
-
- -
-
- - Gardner, Graham and King - - - T-Shirt -
-
- -
-
- - Gardner, Graham and King - - - T-Shirt -
-
- -
-
- - Gardner, Graham and King - - - T-Shirt -
-
- -
-
- - Gardner, Graham and King - - - T-Shirt -
-
-
+/> `; -exports[`Storyshots Categories / CategoryProducts without initial data 1`] = ` +exports[`Storyshots Attributes / Attribute value edit form errors 1`] = `
-
-
- - Products - -
- -
-
-
-
- - - - - - - - - - - - - - - - - -
- Name - - Type -
- No products found -
-
-
+/> +`; + +exports[`Storyshots Attributes / Delete multiple attributes default 1`] = ` +
`; exports[`Storyshots Customers / Address editing dialog default 1`] = ` @@ -926,17 +73,31 @@ exports[`Storyshots Generics / AddressEdit default 1`] = ` class="MuiFormControl-root-id MuiFormControl-fullWidth-id" >
+
+
+
+
+
+
+
+
@@ -1178,17 +453,31 @@ exports[`Storyshots Generics / AddressEdit default 1`] = ` class="MuiFormControl-root-id MuiFormControl-fullWidth-id" >
+ `; +exports[`Storyshots Generics / Assign attributes dialog default 1`] = ` +
+`; + +exports[`Storyshots Generics / Assign attributes dialog errors 1`] = ` +
+`; + +exports[`Storyshots Generics / Assign attributes dialog loading 1`] = ` +
+`; + +exports[`Storyshots Generics / Assign product default 1`] = ` +
+`; + exports[`Storyshots Generics / Autocomplete Menu default 1`] = `
+
+
+
+ `; +exports[`Storyshots Generics / Column picker default 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+`; + exports[`Storyshots Generics / Date default 1`] = `
-
- - Some editable text - -
+ + Some editable text + @@ -2114,6 +1529,129 @@ exports[`Storyshots Generics / Money range to 1`] = `
`; +exports[`Storyshots Generics / MultiAutocompleteSelectField with custom option 1`] = ` +
+
+
+
+
+ + +

+ Value: Afghanistan +

+
+
+
+
+
+ + Afghanistan + + +
+
+
+
+
+
+`; + exports[`Storyshots Generics / MultiAutocompleteSelectField with loaded data 1`] = `
-
+
+ + +

+ Value: Afghanistan +

+
+
+
+
- - -

- Value: afghanistan -

-
-
-
-

- Afghanistan -

-
- -
+ +
- +
@@ -2235,104 +1786,117 @@ exports[`Storyshots Generics / MultiAutocompleteSelectField with loading data 1`
-
+
-
-
-
-
+
+
+
+
+
+ -

+ -

+ + + + +
- +
@@ -2831,7 +2395,11 @@ exports[`Storyshots Generics / PageHeader with title 1`] = `
+ > +
+
`; @@ -2851,31 +2419,35 @@ exports[`Storyshots Generics / PageHeader with title icon bar 1`] = `
- + + + +
@@ -2900,7 +2472,11 @@ exports[`Storyshots Generics / PageHeader without title 1`] = `
+ > +
+
`; @@ -2982,17 +2558,31 @@ exports[`Storyshots Generics / PhoneField default 1`] = ` class="MuiFormControl-root-id" >
+
+
+

+ @@ -3127,17 +2759,31 @@ exports[`Storyshots Generics / Price input with label 1`] = ` class="MuiFormControl-root-id MuiFormControl-fullWidth-id" >
+ @@ -3164,17 +2810,31 @@ exports[`Storyshots Generics / Price input with label and hint 1`] = ` class="MuiFormControl-root-id MuiFormControl-fullWidth-id" >
+ @@ -3206,11 +2866,25 @@ exports[`Storyshots Generics / Price input with no value 1`] = ` class="MuiFormControl-root-id MuiFormControl-fullWidth-id" >
+ @@ -3237,11 +2911,25 @@ exports[`Storyshots Generics / Price input with value 1`] = ` class="MuiFormControl-root-id MuiFormControl-fullWidth-id" >
+
+

+
@@ -3963,7 +3671,7 @@ exports[`Storyshots Generics / SingleAutocompleteSelectField with custom option class="MuiFormControl-root-id MuiFormControl-fullWidth-id" >

`; -exports[`Storyshots Navigation / Menu item create default 1`] = ` +exports[`Storyshots Navigation / Menu item default 1`] = ` +

+`; + +exports[`Storyshots Navigation / Menu item edit 1`] = ` +
+`; + +exports[`Storyshots Navigation / Menu item errors 1`] = `
@@ -6554,19 +6326,7 @@ exports[`Storyshots Orders / OrderShippingMethodEditDialog default 1`] = ` /> `; -exports[`Storyshots Product types / Edit attribute default 1`] = ` -
-`; - -exports[`Storyshots Product types / Edit attribute form errors 1`] = ` -
-`; - -exports[`Storyshots Product types / Edit attribute loading 1`] = ` +exports[`Storyshots Product types / ProductTypeDeleteDialog default 1`] = `
@@ -6622,6 +6382,5164 @@ exports[`Storyshots SiteSettings / Add key dialog form errors 1`] = ` /> `; +exports[`Storyshots Views / Attributes / Attribute details create 1`] = ` +
+
+
+
+
+ Create New Attribute +
+
+
+
+
+
+
+
+
+ + General Information + +
+
+
+
+
+
+ +
+ + +
+
+
+
+ +
+ + +
+

+ This is used internally. Make sure you don’t use spaces +

+
+
+
+ +
+
+
+ Dropdown +
+ + +
+
+
+
+
+
+
+
+ + Attribute Values + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + Admin + + Default Store View + +
+
+
+
+
+
+ + Properties + +
+
+
+
+
+
+ Storefront Properties +
+
+
+`; + +exports[`Storyshots Views / Attributes / Attribute details default 1`] = ` +
+
+
+
+
+ Author +
+
+
+
+
+
+
+
+
+ + General Information + +
+
+
+
+
+
+ +
+ + +
+
+
+
+ +
+ + +
+

+ This is used internally. Make sure you don’t use spaces +

+
+
+
+ +
+
+
+ Dropdown +
+ + +
+
+
+
+
+
+
+
+ + Attribute Values + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + Admin + + Default Store View + +
+
+
+
+
+
+ + Properties + +
+
+
+
+
+
+ Storefront Properties +
+
+
+`; + +exports[`Storyshots Views / Attributes / Attribute details form errors 1`] = ` +
+
+
+
+
+ Author +
+
+
+
+
+
+
+
+
+ + General Information + +
+
+
+
+
+
+ +
+ + +
+

+ Generic form error +

+
+
+
+ +
+ + +
+

+ Generic form error +

+
+
+
+ +
+
+
+ Dropdown +
+ + +
+
+
+
+
+
+
+
+ + Attribute Values + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + Admin + + Default Store View + +
+
+
+
+
+
+ + Properties + +
+
+
+
+
+
+ Storefront Properties +
+
+
+`; + +exports[`Storyshots Views / Attributes / Attribute details loading 1`] = ` +
+
+
+
+
+ + ‌ + +
+
+
+
+
+
+
+
+
+ + General Information + +
+
+
+
+
+
+ +
+ + +
+
+
+
+ +
+ + +
+

+ This is used internally. Make sure you don’t use spaces +

+
+
+
+ +
+
+
+ Dropdown +
+ + +
+
+
+
+
+
+
+
+ + Attribute Values + +
+ +
+
+
+
+ + + + + + + + + + + + + + + +
+ + Admin + + Default Store View + +
+ + + + ‌ + + + + ‌ + + + +
+
+
+
+
+
+ + Properties + +
+
+
+
+
+
+ Storefront Properties +
+
+
+`; + +exports[`Storyshots Views / Attributes / Attribute details multiple select input 1`] = ` +
+
+
+
+
+ Author +
+
+
+
+
+
+
+
+
+ + General Information + +
+
+
+
+
+
+ +
+ + +
+
+
+
+ +
+ + +
+

+ This is used internally. Make sure you don’t use spaces +

+
+
+
+ +
+
+
+ Multiple Select +
+ + +
+
+
+
+
+
+
+
+ + Attribute Values + +
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + Admin + + Default Store View + +
+
+
+
+
+
+ + Properties + +
+
+
+
+
+
+ Storefront Properties +
+
+
+`; + +exports[`Storyshots Views / Attributes / Attribute details no values 1`] = ` +
+
+
+
+
+ Author +
+
+
+
+
+
+
+
+
+ + General Information + +
+
+
+
+
+
+ +
+ + +
+
+
+
+ +
+ + +
+

+ This is used internally. Make sure you don’t use spaces +

+
+
+
+ +
+
+
+ Dropdown +
+ + +
+
+
+
+
+
+
+
+ + Attribute Values + +
+ +
+
+
+
+ + + + + + + + + + + + + + + +
+ + Admin + + Default Store View + +
+ + + + ‌ + + + + ‌ + + + +
+
+
+
+
+
+ + Properties + +
+
+
+
+
+
+ Storefront Properties +
+
+
+`; + +exports[`Storyshots Views / Attributes / Attribute list default 1`] = ` +
+
+
+
+ Attributes +
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Attribute Code + + Default Label + + Visible + + Searchable + + Use in faceted search +
+
+
+
+`; + +exports[`Storyshots Views / Attributes / Attribute list loading 1`] = ` +
+
+
+
+ Attributes +
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Attribute Code + + Default Label + + Visible + + Searchable + + Use in faceted search +
+
+
+
+`; + +exports[`Storyshots Views / Attributes / Attribute list no data 1`] = ` +
+
+
+
+ Attributes +
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+ Attribute Code + + Default Label + + Visible + + Searchable + + Use in faceted search +
+ No attributes found +
+
+
+
+`; + exports[`Storyshots Views / Authentication / Log in default 1`] = `
+
+
+
+
+
+ - + Add category + + + +
+ > +
+ + No of Rows: + +
+
+
+ 20 +
+ + +
+
+
+
@@ -7430,32 +12476,36 @@ exports[`Storyshots Views / Categories / Category list empty 1`] = `
- + Add category + + + +
-
+ > +
+ + No of Rows: + +
+
+
+ 20 +
+ + +
+
+
+
@@ -7607,32 +12693,36 @@ exports[`Storyshots Views / Categories / Category list loading 1`] = `
- + Add category + + + +
+ > + +
+ > +
+ + No of Rows: + +
+
+
+ 20 +
+ + +
+
+
+
@@ -7820,7 +12962,11 @@ exports[`Storyshots Views / Categories / Create category When loading 1`] = `
+ > +
+
+
+ > +
+
+
+ > +
+
+ -
@@ -9370,14 +14558,28 @@ Ctrl + K" class="MuiFormControl-root-id MuiFormControl-fullWidth-id" >
+
@@ -9399,7 +14601,7 @@ Ctrl + K" + +
+
+

+ Optional +

+
+
+
+
+
+
+ + Search Engine Preview + +
+ +
+
+
+
+
+

+ Add search engine title and description to make this category easier to find +

+
+
+
+
+ + Subcategories + + + Products + +
+
+
+
+ + Products in Coffees + +
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Name + + + Type + + Published + + Price +
+
+
+
+ +
+`; + exports[`Storyshots Views / Collections / Collection details default 1`] = `
+ > +
+
+ -
@@ -13960,14 +20890,28 @@ Ctrl + K" class="MuiFormControl-root-id MuiFormControl-fullWidth-id" >
+
@@ -13989,7 +20933,7 @@ Ctrl + K"