Migrate product create page to new MacawUI (#3372)

Co-authored-by: Michał Droń <droniu@droniu.dev>
Co-authored-by: timur <timuric@gmail.com>
Co-authored-by: wojteknowacki <wojciech.nowacki@saleor.io>
This commit is contained in:
Krzysztof Żuraw 2023-05-04 10:57:18 +02:00 committed by GitHub
parent 4aa648353e
commit 180e3c56e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
102 changed files with 1589 additions and 2215 deletions

View file

@ -263,13 +263,15 @@ describe("Tests for customer", () => {
createCustomer(email, randomName, address, true).then(({ user }) => {
cy.visit(customerDetailsUrl(user.id))
.get(CUSTOMER_DETAILS_SELECTORS.nameInput)
.clearAndType(updatedName)
.clear()
.type(updatedName)
.get(CUSTOMER_DETAILS_SELECTORS.lastNameInput)
.clearAndType(updatedName)
.get(CUSTOMER_DETAILS_SELECTORS.noteInput)
.clearAndType(updatedName)
.get(CUSTOMER_DETAILS_SELECTORS.emailInput)
.clearAndType(`${updatedName}@example.com`)
.clear()
.type(`${updatedName}@example.com`)
.addAliasToGraphRequest("UpdateCustomer")
.get(BUTTON_SELECTORS.confirm)
.click()

View file

@ -193,7 +193,9 @@ describe("As an admin I want to manage warehouses", () => {
warehouse = warehouseResp;
cy.visit(warehouseDetailsUrl(warehouse.id))
.get(WAREHOUSES_DETAILS.nameInput)
.clearAndType(updatedName)
.clear()
.type(updatedName)
// .clearAndType(updatedName)
.fillUpBasicAddress(secondUsAddress)
.addAliasToGraphRequest("WarehouseUpdate")
.get(BUTTON_SELECTORS.confirm)

View file

@ -162,7 +162,9 @@ describe("Tests for pages", () => {
.then(({ page }) => {
cy.visit(pageDetailsUrl(page.id))
.get(PAGE_DETAILS_SELECTORS.nameInput)
.clearAndType(updatedName)
.clear()
.type(updatedName)
// .clearAndType(updatedName)
.get(PAGE_DETAILS_SELECTORS.isNotPublishedCheckbox)
.click()
.addAliasToGraphRequest("PageUpdate")

View file

@ -192,7 +192,6 @@ describe("Updating products without sku", () => {
.get(SHARED_ELEMENTS.skeleton)
.should("not.exist")
.get(VARIANTS_SELECTORS.skuTextField)
.find("input")
.clear()
.addAliasToGraphRequest("VariantUpdate")
.get(VARIANTS_SELECTORS.variantNameInput)

View file

@ -1,7 +1,7 @@
export const AVAILABLE_CHANNELS_FORM = {
manageChannelsButton: "[data-test-id='channels-availability-manage-button']",
assignedChannels: "[data-test-id='expand-icon']",
publishedRadioButtons: "[name*='isPublished']",
publishedRadioButtons: "[name*='isPublished'] > ",
availableForPurchaseRadioButtons: "[name*='isAvailableForPurchase']",
radioButtonsValueTrue: "[value='true']",
radioButtonsValueFalse: "[value='false']",

View file

@ -6,6 +6,6 @@ export const PAGE_DETAILS_SELECTORS = {
isNotPublishedCheckbox: '[name="isPublished"][value=false]',
uploadFileButton: '[data-test-id="button-upload-file"]',
richTextEditorAttributeValue: '[class*="ce-paragraph"]',
booleanAttributeValueCheckbox: '[name*="attribute:"][type="checkbox"]',
booleanAttributeValueCheckbox: '[role="checkbox"]',
numericAttributeValueInput: '[name*="attribute:"]',
};

View file

@ -10,7 +10,7 @@ Cypress.Commands.add("clearAndType", { prevSubject: true }, (subject, text) => {
if (subject.find("[contenteditable]").length > 0) {
cy.wrap(subject).find("[contenteditable]").clear().type(text);
} else {
cy.wrap(subject).clear().type(text);
cy.wrap(subject).clear({ force: true }).type(text);
}
});
});

View file

@ -9,7 +9,7 @@ export function createAttributeWithInputType({
entityType,
numericSystemType,
swatchImage,
valueRequired = true
valueRequired = true,
}) {
fillUpAttributeCreateFields({ name, attributeType, valueRequired });
if (
@ -34,7 +34,7 @@ export function createAttributeWithInputType({
export function fillUpAttributeCreateFields({
name,
attributeType,
valueRequired
valueRequired,
}) {
fillUpAttributeNameAndCode(name);
cy.get(ATTRIBUTES_DETAILS.inputTypeSelect)
@ -49,9 +49,11 @@ export function fillUpAttributeCreateFields({
export function fillUpAttributeNameAndCode(name, code = name) {
return cy
.get(ATTRIBUTES_DETAILS.nameInput)
.clearAndType(name)
.clear()
.type(name)
.get(ATTRIBUTES_DETAILS.codeInput)
.clearAndType(code);
.clear()
.type(code);
}
export function saveAttribute() {
@ -63,9 +65,7 @@ export function saveAttribute() {
}
export function submitAttribute() {
cy.get(BUTTON_SELECTORS.confirm)
.click()
.confirmationMessageShouldDisappear();
cy.get(BUTTON_SELECTORS.confirm).click().confirmationMessageShouldDisappear();
}
export function addSingleValue(valueName) {

View file

@ -25,6 +25,7 @@ export function createCollection(collectionName, isPublished, channel) {
.get(AVAILABLE_CHANNELS_FORM.availableChannel)
.click()
.get(`${AVAILABLE_CHANNELS_FORM.publishedRadioButtons}${publishedSelector}`)
.first()
.click();
return saveCollection().its("response.body.data.collectionCreate.collection");
}

View file

@ -35,7 +35,6 @@ export function createVariant({
cy.get(VARIANTS_SELECTORS.saveButton)
.click()
.get(VARIANTS_SELECTORS.skuTextField)
.find("input")
.should("be.enabled")
.get(BUTTON_SELECTORS.back)
.click()
@ -86,7 +85,7 @@ export function fillUpVariantDetails({
cy.get(VARIANTS_SELECTORS.variantNameInput).type(variantName);
}
if (sku) {
cy.get(VARIANTS_SELECTORS.skuTextField).type(sku);
cy.get(VARIANTS_SELECTORS.skuTextField).click({ force: true }).type(sku);
}
if (warehouseName) {
cy.get(VARIANTS_SELECTORS.addWarehouseButton).click();
@ -114,7 +113,7 @@ export function fillUpVariantAttributeAndSku({ attributeName, sku }) {
.contains(attributeName)
.click();
if (sku) {
cy.get(VARIANTS_SELECTORS.skuTextField).type(sku);
cy.get(VARIANTS_SELECTORS.skuTextField).click({ force: true }).type(sku);
}
}

View file

@ -37,6 +37,7 @@ function updateProductManageInChannel(productUrl, manageSelector) {
.get(AVAILABLE_CHANNELS_FORM.assignedChannels)
.click()
.get(manageSelector)
.first()
.click()
.waitForProgressBarToNotBeVisible()
.addAliasToGraphRequest("ProductChannelListingUpdate")

View file

@ -80,7 +80,9 @@ export function selectProductsOutOfStock() {
}
export function selectFilterBy(filter) {
return showFilters().get(PRODUCTS_LIST.filters.filterBy[filter]).click();
return showFilters()
.get(PRODUCTS_LIST.filters.filterBy[filter])
.click({ timeout: 1000, force: true });
}
export function selectFilterByAttribute(attributeSlug) {
@ -106,6 +108,8 @@ export function selectChannel(channelSlug) {
export function submitFilters() {
cy.addAliasToGraphRequest("ProductList")
.get(BUTTON_SELECTORS.submit)
.scrollIntoView()
.should("be.visible")
.click()
.waitForRequestAndCheckIfNoErrors("@ProductList")
.get(PRODUCTS_LIST.emptyProductRow)

View file

@ -26,9 +26,11 @@ export function fillUpShippingZoneData({
channelName,
}) {
cy.get(SHIPPING_ZONE_DETAILS.nameInput)
.clearAndType(shippingName)
.clear()
.type(shippingName)
.get(SHIPPING_ZONE_DETAILS.descriptionInput)
.clearAndType(shippingName)
.clear()
.type(shippingName)
.get(BUTTON_SELECTORS.confirm)
.click()
.confirmationMessageShouldAppear()

View file

@ -21,7 +21,9 @@ export function updateTranslationToCategory({
.get(SHARED_ELEMENTS.skeleton)
.should("not.exist")
.get(ELEMENT_TRANSLATION.translationInputField)
.clearAndType(translatedName)
.clear()
.type(translatedName)
// .clearAndType(translatedName)
.get(BUTTON_SELECTORS.confirm)
.click()
.confirmationMessageShouldDisappear()
@ -37,14 +39,17 @@ export function updateTranslationToCategory({
.get(ELEMENT_TRANSLATION.editSeoTitleButton)
.click()
.get(ELEMENT_TRANSLATION.translationInputField)
.clearAndType(translatedSeoTitle)
.clear()
.type(translatedSeoTitle)
.get(BUTTON_SELECTORS.confirm)
.click()
.confirmationMessageShouldDisappear()
.get(ELEMENT_TRANSLATION.editSeoDescriptionButton)
.click()
.get(ELEMENT_TRANSLATION.translationInputField)
.clearAndType(translatedSeoDescription)
.clear()
.type(translatedSeoDescription)
// .clearAndType(translatedSeoDescription)
.get(BUTTON_SELECTORS.confirm)
.click()
.confirmationMessageShouldDisappear();

View file

@ -1614,6 +1614,10 @@
"context": "area units type",
"string": "Area"
},
"AD1PlC": {
"context": "channels availability text",
"string": "In {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}"
},
"ADTNND": {
"context": "product type",
"string": "Physical"
@ -8100,10 +8104,6 @@
"context": "voucher discount type",
"string": "Fixed Amount"
},
"vY2lpx": {
"context": "channels availability text",
"string": "Available at {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}"
},
"vZMs8f": {
"context": "default product variant indicator",
"string": "Default"
@ -8497,6 +8497,9 @@
"context": "voucher requirements, header",
"string": "Minimum Requirements"
},
"yi1HSj": {
"string": "({numberOfCharacters} of {maxCharacters} characters)"
},
"ymo+cm": {
"context": "voucher discount",
"string": "All products"

61
package-lock.json generated
View file

@ -27,7 +27,7 @@
"@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0",
"@saleor/macaw-ui": "0.8.0-pre.68",
"@saleor/macaw-ui": "0.8.0-pre.73",
"@saleor/sdk": "^0.5.0",
"@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6",
@ -7976,9 +7976,9 @@
}
},
"node_modules/@saleor/macaw-ui": {
"version": "0.8.0-pre.68",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.68.tgz",
"integrity": "sha512-4P1Ec4dpNk0MyejtosBZLd0EHl3pOBY1aFt09/R0Qpdiyu2+O7Jh/O1pwG+cpaG7J6hcrtMGsfYfLl24PwIAAA==",
"version": "0.8.0-pre.73",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.73.tgz",
"integrity": "sha512-7+bFDATTV8ZjX3dpU20z5QMUMSZHoNgidLWEzUBkHWk6EgiDf+V5Mn2t8Eexd34uLOIKCQ0j4f/mVG+slRsj/w==",
"dependencies": {
"@dessert-box/react": "^0.4.0",
"@floating-ui/react-dom-interactions": "^0.5.0",
@ -7992,14 +7992,17 @@
"@radix-ui/react-select": "^1.2.0",
"@radix-ui/react-toggle": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.5",
"@vanilla-extract/css-utils": "^0.1.3",
"clsx": "^1.1.1",
"downshift": "^6.1.7",
"downshift7": "npm:downshift@7.6.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react-inlinesvg": "^3.0.1"
},
"engines": {
"node": ">=16 <19"
"node": ">=16 <19",
"pnpm": ">=8"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
@ -16986,6 +16989,27 @@
"react": ">=16.12.0"
}
},
"node_modules/downshift7": {
"name": "downshift",
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.0.tgz",
"integrity": "sha512-VSoTVynTAsabou/hbZ6HJHUVhtBiVOjQoBsCPcQq5eAROIGP+9XKMp9asAKQ3cEcUP4oe0fFdD2pziUjhFY33Q==",
"dependencies": {
"@babel/runtime": "^7.14.8",
"compute-scroll-into-view": "^2.0.4",
"prop-types": "^15.7.2",
"react-is": "^17.0.2",
"tslib": "^2.3.0"
},
"peerDependencies": {
"react": ">=16.12.0"
}
},
"node_modules/downshift7/node_modules/compute-scroll-into-view": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
"integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
},
"node_modules/duplexer": {
"version": "0.1.2",
"devOptional": true,
@ -43347,9 +43371,9 @@
}
},
"@saleor/macaw-ui": {
"version": "0.8.0-pre.68",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.68.tgz",
"integrity": "sha512-4P1Ec4dpNk0MyejtosBZLd0EHl3pOBY1aFt09/R0Qpdiyu2+O7Jh/O1pwG+cpaG7J6hcrtMGsfYfLl24PwIAAA==",
"version": "0.8.0-pre.73",
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.73.tgz",
"integrity": "sha512-7+bFDATTV8ZjX3dpU20z5QMUMSZHoNgidLWEzUBkHWk6EgiDf+V5Mn2t8Eexd34uLOIKCQ0j4f/mVG+slRsj/w==",
"requires": {
"@dessert-box/react": "^0.4.0",
"@floating-ui/react-dom-interactions": "^0.5.0",
@ -43363,8 +43387,10 @@
"@radix-ui/react-select": "^1.2.0",
"@radix-ui/react-toggle": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.5",
"@vanilla-extract/css-utils": "^0.1.3",
"clsx": "^1.1.1",
"downshift": "^6.1.7",
"downshift7": "npm:downshift@7.6.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react-inlinesvg": "^3.0.1"
@ -49697,6 +49723,25 @@
"tslib": "^2.3.0"
}
},
"downshift7": {
"version": "npm:downshift@7.6.0",
"resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.0.tgz",
"integrity": "sha512-VSoTVynTAsabou/hbZ6HJHUVhtBiVOjQoBsCPcQq5eAROIGP+9XKMp9asAKQ3cEcUP4oe0fFdD2pziUjhFY33Q==",
"requires": {
"@babel/runtime": "^7.14.8",
"compute-scroll-into-view": "^2.0.4",
"prop-types": "^15.7.2",
"react-is": "^17.0.2",
"tslib": "^2.3.0"
},
"dependencies": {
"compute-scroll-into-view": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
"integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
}
}
},
"duplexer": {
"version": "0.1.2",
"devOptional": true

View file

@ -34,7 +34,7 @@
"@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0",
"@saleor/macaw-ui": "0.8.0-pre.68",
"@saleor/macaw-ui": "0.8.0-pre.73",
"@saleor/sdk": "^0.5.0",
"@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6",

View file

@ -4,7 +4,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import CardSpacer from "@dashboard/components/CardSpacer";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata/Metadata";
import { Metadata } from "@dashboard/components/Metadata/Metadata";
import { MetadataFormData } from "@dashboard/components/Metadata/types";
import Savebar from "@dashboard/components/Savebar";
import { ListSettingsUpdate } from "@dashboard/components/TablePagination";

View file

@ -1,9 +1,9 @@
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { CardSpacer } from "@dashboard/components/CardSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata";
import { Metadata } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm";
import { SeoForm } from "@dashboard/components/SeoForm";
import { ProductErrorFragment } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";

View file

@ -8,9 +8,9 @@ import { Button } from "@dashboard/components/Button";
import { CardSpacer } from "@dashboard/components/CardSpacer";
import CardTitle from "@dashboard/components/CardTitle";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata/Metadata";
import { Metadata } from "@dashboard/components/Metadata/Metadata";
import Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm";
import { SeoForm } from "@dashboard/components/SeoForm";
import { Tab, TabContainer } from "@dashboard/components/Tab";
import { CategoryDetailsQuery, ProductErrorFragment } from "@dashboard/graphql";
import { SubmitPromise } from "@dashboard/hooks/useForm";

View file

@ -4,9 +4,9 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { CardSpacer } from "@dashboard/components/CardSpacer";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata";
import { Metadata } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm";
import { SeoForm } from "@dashboard/components/SeoForm";
import {
CollectionChannelListingErrorFragment,
CollectionErrorFragment,

View file

@ -4,9 +4,9 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { CardSpacer } from "@dashboard/components/CardSpacer";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata/Metadata";
import { Metadata } from "@dashboard/components/Metadata/Metadata";
import Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm";
import { SeoForm } from "@dashboard/components/SeoForm";
import {
CollectionChannelListingErrorFragment,
CollectionDetailsQuery,

View file

@ -19,10 +19,7 @@ interface TopNavMenuProps {
dataTestId?: string;
}
export const TopNavMenu: React.FC<TopNavMenuProps> = ({
items,
dataTestId,
}) => (
export const Menu: React.FC<TopNavMenuProps> = ({ items, dataTestId }) => (
<Dropdown data-test-id={dataTestId}>
<Dropdown.Trigger>
<Button

View file

@ -13,7 +13,7 @@ interface TopNavProps {
isAlignToRight?: boolean;
}
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
export const Root: React.FC<PropsWithChildren<TopNavProps>> = ({
title,
href,
withoutBorder = false,

View file

@ -1,3 +1,8 @@
export * from "./TopNav";
import { Menu } from "./Menu";
import { Root } from "./Root";
export * from "./TopNavLink";
export * from "./TopNavWrapper";
export const TopNav = Object.assign(Root, {
Menu,
});

View file

@ -0,0 +1,33 @@
import {
PageErrorWithAttributesFragment,
ProductErrorWithAttributesFragment,
} from "@dashboard/graphql";
import React from "react";
import AttributeRow from "./AttributeRow";
import { AttributeRowProps } from "./types";
type AttributeListItemProps = Omit<AttributeRowProps, "error"> & {
errors: Array<
ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment
>;
onAttributeSelectBlur: () => void;
};
export const AttributeListItem: React.FC<AttributeListItemProps> = ({
errors,
attribute,
onAttributeSelectBlur,
...props
}) => {
const error = errors.find(err => err.attributes?.includes(attribute.id));
return (
<AttributeRow
attribute={attribute}
error={error}
onAttributeSelectBlur={onAttributeSelectBlur}
{...props}
/>
);
};

View file

@ -1,6 +1,5 @@
import { inputTypeMessages } from "@dashboard/attributes/components/AttributeDetails/messages";
import { getMeasurementUnitMessage } from "@dashboard/attributes/components/AttributeDetails/utils";
import BasicAttributeRow from "@dashboard/components/Attributes/BasicAttributeRow";
import { BasicAttributeRow } from "@dashboard/components/Attributes/BasicAttributeRow";
import ExtendedAttributeRow from "@dashboard/components/Attributes/ExtendedAttributeRow";
import { attributeRowMessages } from "@dashboard/components/Attributes/messages";
import { SwatchRow } from "@dashboard/components/Attributes/SwatchRow";
@ -13,8 +12,6 @@ import {
getSingleChoices,
getSingleDisplayValue,
} from "@dashboard/components/Attributes/utils";
import Checkbox from "@dashboard/components/Checkbox";
import { DateTimeField } from "@dashboard/components/DateTimeField";
import FileUploadField from "@dashboard/components/FileUploadField";
import MultiAutocompleteSelectField from "@dashboard/components/MultiAutocompleteSelectField";
import RichTextEditor from "@dashboard/components/RichTextEditor";
@ -22,11 +19,12 @@ import SingleAutocompleteSelectField from "@dashboard/components/SingleAutocompl
import SortableChipsField from "@dashboard/components/SortableChipsField";
import { AttributeInputTypeEnum } from "@dashboard/graphql";
import { commonMessages } from "@dashboard/intl";
import { InputAdornment, TextField } from "@material-ui/core";
import { TextField } from "@material-ui/core";
import { Box, Checkbox, Input, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import { useStyles } from "./styles";
import { DateTimeField } from "../DateTimeField";
import { AttributeRowProps } from "./types";
const AttributeRow: React.FC<AttributeRowProps> = ({
@ -47,7 +45,6 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
richTextGetters,
}) => {
const intl = useIntl();
const classes = useStyles();
switch (attribute.data.inputType) {
case AttributeInputTypeEnum.REFERENCE:
@ -77,7 +74,6 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
return (
<BasicAttributeRow label={attribute.label}>
<FileUploadField
className={classes.fileField}
disabled={disabled}
loading={loading}
file={getFileChoice(attribute)}
@ -93,7 +89,10 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
);
case AttributeInputTypeEnum.DROPDOWN:
return (
<BasicAttributeRow label={attribute.label}>
<BasicAttributeRow
label={attribute.label}
id={`attribute:${attribute.label}`}
>
<SingleAutocompleteSelectField
choices={getSingleChoices(attributeValues)}
disabled={disabled}
@ -102,6 +101,7 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
error={!!error}
helperText={getErrorMessage(error, intl)}
name={`attribute:${attribute.label}`}
id={`attribute:${attribute.label}`}
label={intl.formatMessage(attributeRowMessages.valueLabel)}
value={attribute.value[0]}
onChange={event => onChange(attribute.id, event.target.value)}
@ -130,18 +130,19 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
<BasicAttributeRow
label={attribute.label}
description={intl.formatMessage(inputTypeMessages.plainText)}
id={`attribute:${attribute.label}`}
>
<TextField
fullWidth
multiline
<Input
disabled={disabled}
error={!!error}
helperText={getErrorMessage(error, intl)}
label={intl.formatMessage(attributeRowMessages.valueLabel)}
name={`attribute:${attribute.label}`}
onChange={event => onChange(attribute.id, event.target.value)}
type="text"
value={attribute.value[0]}
size="small"
id={`attribute:${attribute.label}`}
helperText={getErrorMessage(error, intl)}
/>
</BasicAttributeRow>
);
@ -157,8 +158,10 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
<BasicAttributeRow
label={attribute.label}
description={intl.formatMessage(inputTypeMessages.richText)}
id={`attribute:${attribute.label}`}
>
{getShouldMount(attribute.id) && (
<Box __minWidth={210}>
<RichTextEditor
defaultValue={defaultValue}
editorRef={getMountEditor(attribute.id)}
@ -168,59 +171,72 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
error={!!error}
label={intl.formatMessage(attributeRowMessages.valueLabel)}
helperText={getErrorMessage(error, intl)}
id={`attribute:${attribute.label}`}
/>
</Box>
)}
</BasicAttributeRow>
);
case AttributeInputTypeEnum.NUMERIC:
return (
<BasicAttributeRow label={attribute.label}>
<TextField
fullWidth
<BasicAttributeRow
label={attribute.label}
id={`attribute:${attribute.label}`}
>
<Input
disabled={disabled}
error={!!error}
helperText={getErrorMessage(error, intl)}
label={intl.formatMessage(attributeRowMessages.valueLabel)}
name={`attribute:${attribute.label}`}
id={`attribute:${attribute.label}`}
onChange={event => onChange(attribute.id, event.target.value)}
type="number"
value={attribute.value[0]}
InputProps={
attribute.data.unit && {
endAdornment: (
<InputAdornment position="end">
{getMeasurementUnitMessage(
attribute.data.unit,
intl.formatMessage,
)}
</InputAdornment>
),
}
}
size="small"
helperText={getErrorMessage(error, intl)}
/>
</BasicAttributeRow>
);
case AttributeInputTypeEnum.BOOLEAN:
return (
<BasicAttributeRow label={attribute.label}>
<div className={classes.pullRight}>
<Box as="li" display="flex" gap={5} alignItems="center" padding={3}>
<Box data-test-id="attribute-value">
<Box
display="flex"
gap={2}
flexDirection="column"
alignItems="flex-end"
>
<Checkbox
disabled={disabled}
name={`attribute:${attribute.label}`}
onChange={event =>
onChange(attribute.id, JSON.stringify(event.target.checked))
}
onCheckedChange={checked => onChange(attribute.id, checked)}
checked={JSON.parse(attribute.value[0] ?? "false")}
className={classes.pullRight}
helperText={getErrorMessage(error, intl)}
error={!!error}
id={`attribute:${attribute.label}`}
/>
</div>
</BasicAttributeRow>
<Text variant="caption" color="textCriticalDefault">
{getErrorMessage(error, intl)}
</Text>
</Box>
</Box>
<Box
data-test-id="attribute-label"
as="label"
htmlFor={`attribute:${attribute.label}`}
display="flex"
gap={3}
cursor="pointer"
>
<Text>{attribute.label}</Text>
</Box>
</Box>
);
case AttributeInputTypeEnum.DATE:
return (
<BasicAttributeRow label={attribute.label} flexValueContainer>
<BasicAttributeRow
label={attribute.label}
id={`attribute:${attribute.label}`}
>
<TextField
fullWidth
disabled={disabled}
@ -228,6 +244,7 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
helperText={getErrorMessage(error, intl)}
label={intl.formatMessage(commonMessages.date)}
name={`attribute:${attribute.label}`}
id={`attribute:${attribute.label}`}
onChange={event => onChange(attribute.id, event.target.value)}
type="date"
value={attribute.value[0]}
@ -237,7 +254,7 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
);
case AttributeInputTypeEnum.DATE_TIME:
return (
<BasicAttributeRow label={attribute.label} flexValueContainer>
<BasicAttributeRow label={attribute.label}>
<DateTimeField
fullWidth
name={`attribute:${attribute.label}`}

View file

@ -1,6 +1,4 @@
import { AttributeReference } from "@dashboard/attributes/utils/data";
import CardTitle from "@dashboard/components/CardTitle";
import Hr from "@dashboard/components/Hr";
import {
AttributeEntityTypeEnum,
AttributeInputTypeEnum,
@ -13,13 +11,12 @@ import {
import { FormsetAtomicData } from "@dashboard/hooks/useFormset";
import { FetchMoreProps } from "@dashboard/types";
import { RichTextGetters } from "@dashboard/utils/richText/useMultipleRichText";
import { Card, CardContent, Typography } from "@material-ui/core";
import { ChevronIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import clsx from "clsx";
import { Accordion, Box, Divider, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
import AttributeRow from "./AttributeRow";
import { DashboardCard } from "../Card";
import { AttributeListItem } from "./AttributeListItem";
import { AttributeRowHandlers, VariantAttributeScope } from "./types";
export interface AttributeInputData {
@ -49,61 +46,6 @@ export interface AttributesProps extends AttributeRowHandlers {
richTextGetters: RichTextGetters<string>;
}
const useStyles = makeStyles(
theme => ({
attributeSection: {
"&:last-of-type": {
paddingBottom: 0,
},
padding: theme.spacing(2, 0),
},
attributeSectionLabel: {
alignItems: "center",
display: "flex",
},
card: {
overflow: "visible",
},
cardContent: {
"&:last-child": {
paddingBottom: theme.spacing(2),
},
paddingTop: theme.spacing(1),
},
expansionBar: {
display: "flex",
},
expansionBarButton: {
padding: 4,
marginBottom: theme.spacing(1),
},
expansionBarButtonIcon: {
transition: theme.transitions.duration.short + "ms",
},
expansionBarLabel: {
color: theme.palette.text.disabled,
fontSize: 14,
},
expansionBarLabelContainer: {
alignItems: "center",
display: "flex",
flex: 1,
},
rotate: {
transform: "rotate(180deg)",
},
uploadFileButton: {
float: "right",
},
uploadFileContent: {
color: theme.palette.primary.main,
float: "right",
fontSize: theme.typography.body1.fontSize,
},
}),
{ name: "Attributes" },
);
const messages = defineMessages({
attributesNumber: {
id: "z0gGP+",
@ -117,7 +59,7 @@ const messages = defineMessages({
},
});
const Attributes: React.FC<AttributesProps> = ({
export const Attributes: React.FC<AttributesProps> = ({
attributes,
attributeValues,
errors,
@ -127,66 +69,50 @@ const Attributes: React.FC<AttributesProps> = ({
...props
}) => {
const intl = useIntl();
const classes = useStyles({});
const [expanded, setExpansionStatus] = React.useState(true);
const toggleExpansion = () => setExpansionStatus(!expanded);
return (
<Card className={classes.card}>
<CardTitle title={title || intl.formatMessage(messages.header)} />
<CardContent className={classes.cardContent}>
<div className={classes.expansionBar}>
<div className={classes.expansionBarLabelContainer}>
<Typography className={classes.expansionBarLabel} variant="caption">
<DashboardCard>
<DashboardCard.Title>
{title || intl.formatMessage(messages.header)}
</DashboardCard.Title>
<DashboardCard.Content>
<Box display="flex" flexDirection="column" gap={5}>
<Accordion defaultValue="attributes-accordion">
<Accordion.Item value="attributes-accordion">
<Accordion.Item.Trigger buttonDataTestId="attributes-expand">
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage
{...messages.attributesNumber}
values={{
number: attributes.length,
}}
/>
</Typography>
</div>
<IconButton
variant="secondary"
hoverOutline={false}
className={classes.expansionBarButton}
onClick={toggleExpansion}
data-test-id="attributes-expand"
>
<ChevronIcon
className={clsx(classes.expansionBarButtonIcon, {
[classes.rotate]: expanded,
})}
/>
</IconButton>
</div>
{expanded && attributes.length > 0 && (
<>
<Hr />
{attributes.map((attribute, attributeIndex) => {
const error = errors.find(err =>
err.attributes?.includes(attribute.id),
);
return (
</Text>
</Accordion.Item.Trigger>
<Accordion.Item.Content>
{attributes.length > 0 && (
<ul>
<Divider />
{attributes.map((attribute, attributeIndex) => (
<React.Fragment key={attribute.id}>
{attributeIndex > 0 && <Hr />}
<AttributeRow
{attributeIndex > 0 && <Divider />}
<AttributeListItem
attribute={attribute}
errors={errors}
attributeValues={attributeValues}
error={error}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={richTextGetters}
{...props}
/>
</React.Fragment>
);
})}
</>
))}
</ul>
)}
</CardContent>
</Card>
</Accordion.Item.Content>
</Accordion.Item>
</Accordion>
</Box>
</DashboardCard.Content>
</DashboardCard>
);
};
Attributes.displayName = "Attributes";
export default Attributes;

View file

@ -1,58 +1,58 @@
import Grid from "@dashboard/components/Grid";
import { Typography } from "@material-ui/core";
import HelpOutline from "@material-ui/icons/HelpOutline";
import { Tooltip } from "@saleor/macaw-ui/next";
import clsx from "clsx";
import { Box, InfoIcon, sprinkles, Text, Tooltip } from "@saleor/macaw-ui/next";
import React from "react";
import { useBasicAttributeStyles } from "./styles";
interface BasicAttributeRowProps {
label: string | React.ReactNode;
description?: string | React.ReactNode;
flexValueContainer?: boolean;
id?: string;
clickableLabel?: boolean;
}
const BasicAttributeRow: React.FC<BasicAttributeRowProps> = ({
export const BasicAttributeRow: React.FC<BasicAttributeRowProps> = ({
label,
description,
children,
flexValueContainer,
}) => {
const classes = useBasicAttributeStyles();
return (
<Grid className={classes.attributeSection} variant="uniform">
<div
className={classes.attributeSectionLabel}
data-test-id="attribute-label"
id,
clickableLabel = false,
}) => (
<Box
as="li"
justifyContent="space-between"
alignItems="center"
paddingY={3}
paddingX={2}
display="grid"
gridTemplateColumns={2}
gap={8}
>
<Typography>
{label}
<Box
data-test-id="attribute-label"
as="label"
htmlFor={id}
display="flex"
gap={3}
cursor={clickableLabel ? "pointer" : "auto"}
>
<Text>{label}</Text>
{description && (
<Tooltip>
<Tooltip.Trigger>
<HelpOutline className={classes.tooltipIcon} />
<Box>
<InfoIcon
size="small"
className={sprinkles({
display: "block",
})}
/>
</Box>
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Content side="top">
<Tooltip.Arrow />
{description}
</Tooltip.Content>
</Tooltip>
)}
</Typography>
</div>
<div
data-test-id="attribute-value"
className={clsx(classes.value, {
[classes.flex]: flexValueContainer,
})}
>
{children}
</div>
</Grid>
);
};
BasicAttributeRow.displayName = "BasicAttributeRow";
export default BasicAttributeRow;
</Box>
<Box data-test-id="attribute-value">{children}</Box>
</Box>
);

View file

@ -1,10 +1,6 @@
import { Button } from "@dashboard/components/Button";
import Grid from "@dashboard/components/Grid";
import { Typography } from "@material-ui/core";
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { useExtendedAttributeStyles } from "./styles";
interface ExtendedAttributeRowProps {
label: string;
selectLabel: string;
@ -14,30 +10,28 @@ interface ExtendedAttributeRowProps {
const ExtendedAttributeRow: React.FC<ExtendedAttributeRowProps> = props => {
const { label, selectLabel, disabled, onSelect, children } = props;
const classes = useExtendedAttributeStyles(props);
return (
<>
<Grid className={classes.attributeSection} variant="uniform">
<div
className={classes.attributeSectionLabel}
data-test-id="attribute-label"
<Box
as="li"
display="flex"
justifyContent="space-between"
alignItems="center"
paddingY={3}
>
<Typography>{label}</Typography>
</div>
<div data-test-id="attribute-selector">
<Text data-test-id="attribute-label">{label}</Text>
<Button
className={classes.attributeSectionButton}
disabled={disabled}
variant="tertiary"
variant="secondary"
data-test-id="button-attribute-selector"
onClick={onSelect}
type="button"
>
{selectLabel}
</Button>
</div>
</Grid>
<div data-test-id="attribute-value">{children}</div>
</Box>
<Box data-test-id="attribute-value">{children}</Box>
</>
);
};

View file

@ -1,4 +1,4 @@
import BasicAttributeRow from "@dashboard/components/Attributes/BasicAttributeRow";
import { BasicAttributeRow } from "@dashboard/components/Attributes/BasicAttributeRow";
import {
getErrorMessage,
getSingleDisplayValue,
@ -38,7 +38,10 @@ export const SwatchRow: React.FC<SwatchRowProps> = ({
const value = attribute.data.values.find(getBySlug(attribute.value[0]));
return (
<BasicAttributeRow label={attribute.label}>
<BasicAttributeRow
label={attribute.label}
id={`attribute:${attribute.label}`}
>
<SingleAutocompleteSelectField
fetchOnFocus
allowCustomValues={false}
@ -65,6 +68,7 @@ export const SwatchRow: React.FC<SwatchRowProps> = ({
error={!!error}
helperText={getErrorMessage(error, intl)}
name={`attribute:${attribute.label}`}
id={`attribute:${attribute.label}`}
value={attribute.value[0]}
onChange={event => onChange(attribute.id, event.target.value)}
fetchChoices={value => fetchAttributeValues(value, attribute.id)}

View file

@ -1,3 +1,2 @@
export { default } from "./Attributes";
export * from "./Attributes";
export * from "./types";

View file

@ -2,13 +2,6 @@ import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
() => ({
fileField: {
float: "right",
},
pullRight: {
display: "flex",
justifyContent: "flex-end",
},
swatchInput: {
paddingTop: 16.5,
paddingBottom: 16.5,
@ -23,62 +16,3 @@ export const useStyles = makeStyles(
}),
{ name: "AttributeRow" },
);
export const useBasicAttributeStyles = makeStyles(
theme => ({
attributeSection: {
"&:last-of-type": {
paddingBottom: 0,
},
padding: theme.spacing(2, 0),
wordBreak: "break-word",
},
attributeSectionLabel: {
alignItems: "center",
display: "flex",
},
flex: {
columnGap: theme.spacing(2) + "px",
display: "flex",
flexDirection: "row",
[theme.breakpoints.down("md")]: {
flexDirection: "column",
rowGap: theme.spacing(2) + "px",
},
},
value: {
"&&": {
overflow: "visible",
},
},
tooltipIcon: {
fill: theme.palette.type === "dark" ? "#FAFAFA" : "#28234A",
fillOpacity: 0.6,
"&:hover": {
fillOpacity: 1,
},
padding: theme.spacing(0.25),
marginLeft: theme.spacing(0.75),
},
}),
{ name: "BasicAttributeRow" },
);
export const useExtendedAttributeStyles = makeStyles(
theme => ({
attributeSection: {
"&:last-of-type": {
paddingBottom: 0,
},
padding: theme.spacing(2, 0),
},
attributeSectionButton: {
float: "right",
},
attributeSectionLabel: {
alignItems: "center",
display: "flex",
},
}),
{ name: "ExtendedAttributeRow" },
);

View file

@ -16,7 +16,7 @@ export enum VariantAttributeScope {
}
export interface AttributeRowHandlers {
onChange: FormsetChange<string>;
onChange: FormsetChange<string | boolean>;
onFileChange: FormsetChange<File>;
onMultiChange: FormsetChange<string>;
onReferencesAddClick: (attribute: AttributeInput) => void;

View file

@ -0,0 +1,8 @@
import { Box, Sprinkles } from "@saleor/macaw-ui/next";
import React from "react";
export const Content: React.FC<Sprinkles> = ({ children, ...rest }) => (
<Box paddingX={9} {...rest}>
{children}
</Box>
);

View file

@ -0,0 +1,8 @@
import { Box, Sprinkles } from "@saleor/macaw-ui/next";
import React from "react";
export const Root: React.FC<Sprinkles> = ({ children, ...rest }) => (
<Box display="flex" flexDirection="column" gap={9} {...rest}>
{children}
</Box>
);

View file

@ -0,0 +1,8 @@
import { Box, Text } from "@saleor/macaw-ui/next";
import React from "react";
export const Title: React.FC = ({ children }) => (
<Box paddingX={9} paddingTop={9}>
<Text variant="heading">{children}</Text>
</Box>
);

View file

@ -0,0 +1,5 @@
import { Content } from "./Content";
import { Root } from "./Root";
import { Title } from "./Title";
export const DashboardCard = Object.assign(Root, { Title, Content });

View file

@ -1,16 +1,18 @@
import { ChannelData } from "@dashboard/channels/utils";
import ControlledCheckbox from "@dashboard/components/ControlledCheckbox";
import Hr from "@dashboard/components/Hr";
import RadioSwitchField from "@dashboard/components/RadioSwitchField";
import useCurrentDate from "@dashboard/hooks/useCurrentDate";
import useDateLocalize from "@dashboard/hooks/useDateLocalize";
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
import { TextField, Typography } from "@material-ui/core";
import clsx from "clsx";
import { TextField } from "@material-ui/core";
import {
Box,
Checkbox,
Divider,
RadioGroup,
Text,
} from "@saleor/macaw-ui/next";
import React, { useState } from "react";
import { useIntl } from "react-intl";
import { useStyles } from "../styles";
import { ChannelOpts, ChannelsAvailabilityError, Messages } from "../types";
import { availabilityItemMessages } from "./messages";
@ -22,7 +24,7 @@ export interface ChannelContentProps {
onChange: (id: string, data: ChannelOpts) => void;
}
const ChannelContent: React.FC<ChannelContentProps> = ({
export const ChannelAvailabilityItemContent: React.FC<ChannelContentProps> = ({
data,
disabled,
errors,
@ -55,7 +57,6 @@ const ChannelContent: React.FC<ChannelContentProps> = ({
);
const [isAvailableDate, setAvailableDate] = useState(false);
const intl = useIntl();
const classes = useStyles({});
const parsedDate = new Date(dateNow);
const todayDateUTC = parsedDate.toISOString().slice(0, 10);
@ -70,57 +71,64 @@ const ChannelContent: React.FC<ChannelContentProps> = ({
);
return (
<div className={classes.container}>
<RadioSwitchField
classes={{
radioLabel: classes.radioLabel,
}}
className={classes.radioField}
disabled={disabled}
firstOptionLabel={
<>
<p className={classes.label}>{messages.visibleLabel}</p>
{isPublished &&
publicationDate &&
Date.parse(publicationDate) < dateNow && (
<span className={classes.secondLabel}>
{messages.visibleSecondLabel ||
visibleMessage(publicationDate)}
</span>
)}
</>
}
name="isPublished"
secondOptionLabel={
<>
<p className={classes.label}>{messages.hiddenLabel}</p>
{publicationDate &&
!isPublished &&
Date.parse(publicationDate) >= dateNow && (
<span className={classes.secondLabel}>
{messages.hiddenSecondLabel}
</span>
)}
</>
}
value={isPublished}
onChange={() => {
<Box display="flex" gap={6} paddingTop={6} flexDirection="column">
<RadioGroup
value={String(isPublished)}
onValueChange={value => {
onChange(id, {
...formData,
isPublished: !isPublished,
isPublished: value === "true",
publicationDate:
!isPublished && !publicationDate ? todayDateUTC : publicationDate,
});
}}
/>
disabled={disabled}
display="flex"
flexDirection="column"
gap={6}
>
<RadioGroup.Item
id={`${id}-isPublished-true`}
value="true"
name="isPublished"
>
<Box display="flex" alignItems="baseline" gap={5}>
<Text>{messages.visibleLabel}</Text>
{isPublished &&
publicationDate &&
Date.parse(publicationDate) < dateNow && (
<Text variant="caption" color="textNeutralSubdued">
{messages.visibleSecondLabel ||
visibleMessage(publicationDate)}
</Text>
)}
</Box>
</RadioGroup.Item>
<RadioGroup.Item
id={`${id}-isPublished-false`}
value="false"
name="isPublished"
>
<Box display="flex" alignItems="baseline" gap={5}>
<Text>{messages.hiddenLabel}</Text>
{publicationDate &&
!isPublished &&
Date.parse(publicationDate) >= dateNow && (
<Text variant="caption" color="textNeutralSubdued">
{messages.hiddenSecondLabel}
</Text>
)}
</Box>
</RadioGroup.Item>
</RadioGroup>
{!isPublished && (
<>
<Typography
className={classes.setPublicationDate}
onClick={() => setPublicationDate(!isPublicationDate)}
<Box display="flex" flexDirection="column" alignItems="start" gap={3}>
<Checkbox
onCheckedChange={(checked: boolean) => setPublicationDate(checked)}
checked={isPublicationDate}
>
{intl.formatMessage(availabilityItemMessages.setPublicationDate)}
</Typography>
</Checkbox>
{isPublicationDate && (
<TextField
error={!!formErrors.publicationDate}
@ -141,64 +149,75 @@ const ChannelContent: React.FC<ChannelContentProps> = ({
publicationDate: e.target.value || null,
})
}
className={classes.date}
InputLabelProps={{
shrink: true,
}}
/>
)}
</>
</Box>
)}
{hasAvailableProps && (
<>
<Hr />
<RadioSwitchField
classes={{
radioLabel: classes.radioLabel,
}}
className={classes.radioField}
<Divider />
<RadioGroup
disabled={disabled}
firstOptionLabel={
<>
<p className={classes.label}>{messages.availableLabel}</p>
name={`channel:isAvailableForPurchase:${id}`}
value={String(isAvailable)}
onValueChange={value =>
onChange(id, {
...formData,
availableForPurchase: !value ? null : availableForPurchase,
isAvailableForPurchase: value === "true",
})
}
display="flex"
flexDirection="column"
gap={6}
>
<RadioGroup.Item
id={`channel:isAvailableForPurchase:${id}-true`}
value="true"
>
<Box display="flex" __alignItems="baseline" gap={5}>
<Text>{messages.availableLabel}</Text>
{isAvailable &&
availableForPurchase &&
Date.parse(availableForPurchase) < dateNow && (
<span className={classes.secondLabel}>
<Text variant="caption" color="textNeutralSubdued">
{visibleMessage(availableForPurchase)}
</span>
</Text>
)}
</>
}
name={`channel:isAvailableForPurchase:${id}`}
secondOptionLabel={
<>
<p className={classes.label}>{messages.unavailableLabel}</p>
</Box>
</RadioGroup.Item>
<RadioGroup.Item
id={`channel:isAvailableForPurchase:${id}-false`}
value="false"
>
<Box display="flex" __alignItems="baseline" gap={5}>
<Text>{messages.unavailableLabel}</Text>
{availableForPurchase && !isAvailable && (
<span className={classes.secondLabel}>
<Text variant="caption" color="textNeutralSubdued">
{messages.availableSecondLabel}
</span>
</Text>
)}
</>
}
value={isAvailable}
onChange={e => {
const { value } = e.target;
return onChange(id, {
...formData,
availableForPurchase: !value ? null : availableForPurchase,
isAvailableForPurchase: value,
});
}}
/>
</Box>
</RadioGroup.Item>
</RadioGroup>
{!isAvailable && (
<>
<Typography
className={classes.setPublicationDate}
onClick={() => setAvailableDate(!isAvailableDate)}
<Box
display="flex"
gap={3}
flexDirection="column"
alignItems="start"
>
<Checkbox
onCheckedChange={(checked: boolean) =>
setAvailableDate(checked)
}
checked={isAvailableDate}
>
{messages.setAvailabilityDateLabel}
</Typography>
</Checkbox>
{isAvailableDate && (
<TextField
error={!!formErrors.availableForPurchaseDate}
@ -224,46 +243,41 @@ const ChannelContent: React.FC<ChannelContentProps> = ({
availableForPurchase: e.target.value,
})
}
className={classes.date}
InputLabelProps={{
shrink: true,
}}
/>
)}
</>
</Box>
)}
</>
)}
{visibleInListings !== undefined && (
<>
<Hr />
<ControlledCheckbox
className={classes.checkbox}
<Divider />
<Checkbox
name={`channel:visibleInListings:${id}`}
id={`channel:visibleInListings:${id}`}
checked={!visibleInListings}
disabled={disabled}
label={
<>
<p className={clsx(classes.label, classes.listingLabel)}>
onCheckedChange={checked => {
onChange(id, {
...formData,
visibleInListings: !checked,
});
}}
>
<Text cursor="pointer">
{intl.formatMessage(availabilityItemMessages.hideInListings)}
</p>
<span className={classes.secondLabel}>
</Text>
</Checkbox>
<Text variant="caption" color="textNeutralSubdued">
{intl.formatMessage(
availabilityItemMessages.hideInListingsDescription,
)}
</span>
</>
}
onChange={e =>
onChange(id, {
...formData,
visibleInListings: !e.target.value,
})
}
/>
</Text>
</>
)}
</div>
</Box>
);
};
export default ChannelContent;

View file

@ -1,94 +1,28 @@
import { ChannelData } from "@dashboard/channels/utils";
import Label from "@dashboard/orders/components/OrderHistory/Label";
import { Typography } from "@material-ui/core";
import { Accordion, AccordionSummary, makeStyles } from "@saleor/macaw-ui";
import { Accordion, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { Messages } from "../types";
const useExpanderStyles = makeStyles(
theme => ({
expanded: {},
root: {
boxShadow: "none",
margin: 0,
padding: 0,
paddingBottom: theme.spacing(2),
"&:before": {
content: "none",
},
"&$expanded": {
margin: 0,
border: "none",
},
},
}),
{ name: "ChannelContentWrapperExpander" },
);
const useSummaryStyles = makeStyles(
theme => ({
expanded: {},
root: {
width: "100%",
border: "none",
margin: 0,
padding: 0,
minHeight: 0,
paddingTop: theme.spacing(2),
"&$expanded": {
minHeight: 0,
padding: theme.spacing(2, 0),
},
},
}),
{ name: "ChannelContentWrapperExpanderSummary" },
);
const useStyles = makeStyles(
() => ({
container: {
display: "flex",
flexDirection: "column",
},
}),
{ name: "ChannelWithVariantAvailabilityItemWrapper" },
);
export interface ChannelContentWrapperProps {
data: ChannelData;
children: React.ReactNode;
messages: Messages;
}
const ChannelContentWrapper: React.FC<ChannelContentWrapperProps> = ({
data,
messages,
children,
}) => {
const expanderClasses = useExpanderStyles();
const summaryClasses = useSummaryStyles();
const classes = useStyles();
const { name } = data;
return (
<Accordion
classes={expanderClasses}
data-test-id="channel-availability-item"
>
<AccordionSummary className={summaryClasses.root}>
<div className={classes.container}>
<Typography>{name}</Typography>
export const ChannelAvailabilityItemWrapper: React.FC<
ChannelContentWrapperProps
> = ({ data: { name }, messages, children }) => (
<Accordion data-test-id="channel-availability-item">
<Accordion.Item value="channel-availability-item" gap={12}>
<Accordion.Item.Trigger buttonDataTestId="expand-icon">
<Text variant={"bodyEmp"}>{name}</Text>
<Label text={messages.availableDateText} />
</div>
</AccordionSummary>
</Accordion.Item.Trigger>
<Accordion.Item.Content paddingLeft={6}>
{children}
</Accordion.Item.Content>
</Accordion.Item>
</Accordion>
);
};
export default ChannelContentWrapper;
);

View file

@ -0,0 +1,2 @@
export * from "./ChannelAvailabilityItemContent";
export * from "./ChannelAvailabilityItemWrapper";

View file

@ -1,18 +1,19 @@
import { Channel as ChannelList, ChannelData } from "@dashboard/channels/utils";
import Hr from "@dashboard/components/Hr";
import { PermissionEnum } from "@dashboard/graphql";
import useDateLocalize from "@dashboard/hooks/useDateLocalize";
import { RequireOnlyOne } from "@dashboard/misc";
import { Typography } from "@material-ui/core";
import { Box, Divider, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import ChannelAvailabilityItemContent from "./Channel/ChannelAvailabilityItemContent";
import ChannelAvailabilityItemWrapper from "./Channel/ChannelAvailabilityItemWrapper";
import ChannelsAvailabilityCardWrapper, {
import {
ChannelAvailabilityItemContent,
ChannelAvailabilityItemWrapper,
} from "./Channel";
import {
ChannelsAvailabilityCardWrapper,
ChannelsAvailabilityWrapperProps,
} from "./ChannelsAvailabilityCardWrapper";
import { useStyles } from "./styles";
import { ChannelOpts, ChannelsAvailabilityError, Messages } from "./types";
import { getChannelsAvailabilityMessages } from "./utils";
@ -36,7 +37,9 @@ export type ChannelsAvailabilityCardProps = RequireOnlyOne<
"channels" | "channelsList"
>;
export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = props => {
export const ChannelsAvailability: React.FC<
ChannelsAvailabilityCardProps
> = props => {
const {
channelsList,
errors = [],
@ -49,7 +52,6 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = pro
} = props;
const intl = useIntl();
const localizeDate = useDateLocalize();
const classes = useStyles({});
const channelsMessages = getChannelsAvailabilityMessages({
messages,
@ -88,12 +90,10 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = pro
: channelsList
? channelsList.map(data => (
<React.Fragment key={data.id}>
<div className={classes.channelItem}>
<div className={classes.channelName}>
<Typography>{data.name}</Typography>
</div>
</div>
<Hr className={classes.hr} />
<Box>
<Text>{data.name}</Text>
</Box>
<Divider />
</React.Fragment>
))
: null}

View file

@ -1,13 +1,10 @@
import { Button } from "@dashboard/components/Button";
import CardTitle from "@dashboard/components/CardTitle";
import Hr from "@dashboard/components/Hr";
import RequirePermissions from "@dashboard/components/RequirePermissions";
import { PermissionEnum } from "@dashboard/graphql";
import { Card, CardContent, Typography } from "@material-ui/core";
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import { useStyles } from "./styles";
import { DashboardCard } from "../Card";
export interface ChannelsAvailabilityWrapperProps {
selectedChannelsCount: number;
@ -17,7 +14,9 @@ export interface ChannelsAvailabilityWrapperProps {
openModal: () => void;
}
export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperProps> = props => {
export const ChannelsAvailabilityCardWrapper: React.FC<
ChannelsAvailabilityWrapperProps
> = props => {
const {
selectedChannelsCount,
allChannelsCount,
@ -26,12 +25,11 @@ export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperPr
openModal,
} = props;
const intl = useIntl();
const classes = useStyles({});
const channelsAvailabilityText = intl.formatMessage(
{
id: "vY2lpx",
id: "AD1PlC",
defaultMessage:
"Available at {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}",
"In {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}",
description: "channels availability text",
},
{
@ -41,19 +39,27 @@ export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperPr
);
return (
<>
<Card>
<CardTitle
title={intl.formatMessage({
<DashboardCard>
<DashboardCard.Title>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box display={"flex"} flexDirection={"column"} gap={3}>
<div>
{intl.formatMessage({
id: "5A6/2C",
defaultMessage: "Availability",
description: "section header",
})}
toolbar={
</div>
{!!channelsAvailabilityText && (
<Text variant={"caption"}>{channelsAvailabilityText}</Text>
)}
</Box>
<RequirePermissions requiredPermissions={managePermissions}>
<Button
onClick={openModal}
data-test-id="channels-availability-manage-button"
type="button"
variant="secondary"
>
{intl.formatMessage({
id: "2i81/P",
@ -62,22 +68,13 @@ export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperPr
})}
</Button>
</RequirePermissions>
}
/>
<CardContent className={classes.card}>
{!!channelsAvailabilityText && (
<>
<Typography className={classes.channelInfo}>
{channelsAvailabilityText}
</Typography>
<Hr className={classes.hr} />
</>
)}
</Box>
</DashboardCard.Title>
<DashboardCard.Content gap={3} display="flex" flexDirection="column">
<Box display="flex" flexDirection="column" gap={8}>
{children}
</CardContent>
</Card>
</>
</Box>
</DashboardCard.Content>
</DashboardCard>
);
};
export default ChannelsAvailabilityWrapper;

View file

@ -1,86 +0,0 @@
import { makeStyles } from "@material-ui/core/styles";
export const useStyles = makeStyles(
theme => ({
container: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
radioField: {
paddingLeft: theme.spacing(1),
},
arrow: {
transition: theme.transitions.duration.short + "ms",
},
card: {
"&:last-child": {
paddingBottom: 0,
},
paddingTop: 0,
},
channelBtn: {
"&:focus": {
outline: "none",
},
background: "transparent",
border: "none",
cursor: "pointer",
textAlign: "left",
},
channelInfo: {
fontSize: 14,
padding: theme.spacing(2, 0),
},
channelItem: {
"&:last-child hr": {
display: "none",
},
padding: theme.spacing(2, 0),
},
channelName: {
alignItems: "center",
display: "flex",
justifyContent: "space-between",
marginBottom: theme.spacing(0.5),
},
checkbox: {
alignItems: "flex-start",
marginTop: 10,
},
date: {
"& svg": {
fill: theme.palette.primary.main,
},
marginTop: theme.spacing(1),
},
hr: {
position: "relative",
},
label: {
lineHeight: 1.2,
},
listingLabel: {
marginTop: 9,
},
radioLabel: {
"& > span": {
padding: theme.spacing(0, 0.5),
},
},
rotate: {
transform: "rotate(180deg)",
},
secondLabel: {
color: theme.palette.text.hint,
fontSize: 12,
},
setPublicationDate: {
color: theme.palette.primary.main,
cursor: "pointer",
fontSize: 14,
paddingBottom: 10,
paddingTop: 0,
},
}),
{ name: "ChannelsAvailabilityCard" },
);

View file

@ -7,11 +7,10 @@ import { commonMessages } from "@dashboard/intl";
import { joinDateTime, splitDateTime } from "@dashboard/misc";
import { TextField } from "@material-ui/core";
import { TextFieldProps } from "@material-ui/core/TextField";
import { Box } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
import { useStyles } from "./styles";
type DateTimeFieldProps = Omit<TextFieldProps, "label" | "error"> & {
onChange: (value: string) => void;
error: ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment;
@ -26,12 +25,11 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
value,
}) => {
const intl = useIntl();
const classes = useStyles();
const parsedValue = value ? splitDateTime(value) : { date: "", time: "" };
return (
<>
<Box display="flex" gap={2}>
<TextField
fullWidth
disabled={disabled}
@ -47,11 +45,6 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
type="date"
value={parsedValue.date}
InputLabelProps={{ shrink: true }}
InputProps={{
classes: {
root: classes.dateField,
},
}}
/>
<TextField
fullWidth
@ -68,12 +61,7 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
type="time"
value={parsedValue.time}
InputLabelProps={{ shrink: true }}
InputProps={{
classes: {
root: classes.timeField,
},
}}
/>
</>
</Box>
);
};

View file

@ -1,28 +0,0 @@
import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles(
theme => ({
dateField: {
borderRadius: "4px 0 0 4px",
},
timeField: {
borderRadius: "0 4px 4px 0",
"& > fieldset": {
borderLeftWidth: "0 !important",
},
},
[theme.breakpoints.down("md")]: {
dateField: {
borderRadius: "4px 4px 0 0",
},
timeField: {
borderRadius: "0 0 4px 4px",
"& > fieldset": {
borderTopWidth: "0 !important",
borderLeftWidth: "1px !important",
},
},
},
}),
{ name: "DateTimeField" },
);

View file

@ -1,8 +1,6 @@
import { Button } from "@dashboard/components/Button";
import { FileFragment } from "@dashboard/graphql";
import { commonMessages } from "@dashboard/intl";
import { Typography } from "@material-ui/core";
import { DeleteIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import { Box, Button, Text, TrashBinIcon } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
@ -19,7 +17,6 @@ export interface FileUploadFieldProps {
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>;
className?: string;
disabled: boolean;
loading: boolean;
file: FileChoiceType;
@ -29,44 +26,17 @@ export interface FileUploadFieldProps {
onFileDelete: () => void;
}
const useStyles = makeStyles(
theme => ({
errorText: {
color: theme.palette.error.light,
},
fileField: {
display: "none",
},
fileUrl: {
color: theme.palette.primary.main,
textDecoration: "none",
},
uploadFileContent: {
alignItems: "center",
color: theme.palette.primary.main,
display: "flex",
fontSize: theme.typography.body1.fontSize,
},
uploadFileName: {
minWidth: "6rem",
},
}),
{ name: "FileUploadField" },
);
const FileUploadField: React.FC<FileUploadFieldProps> = props => {
const {
loading,
disabled,
file,
className,
error,
helperText,
onFileUpload,
onFileDelete,
inputProps,
} = props;
const classes = useStyles({});
const intl = useIntl();
const fileInputAnchor = React.createRef<HTMLInputElement>();
@ -85,52 +55,46 @@ const FileUploadField: React.FC<FileUploadFieldProps> = props => {
return (
<>
<div className={className}>
<Box display="flex" justifyContent="flex-end">
{file.label ? (
<div className={classes.uploadFileContent}>
<div className={classes.uploadFileName}>
<Box display="flex" gap={5} alignItems="center">
<Text variant="caption">
{loading ? (
<Skeleton />
) : (
<a
href={file.file?.url}
target="blank"
className={classes.fileUrl}
>
<a href={file.file?.url} target="blank">
{file.label}
</a>
)}
</div>
<IconButton
</Text>
<Button
icon={<TrashBinIcon />}
variant="secondary"
color="primary"
onClick={handleFileDelete}
disabled={disabled || loading}
data-test-id="button-delete-file"
>
<DeleteIcon />
</IconButton>
</div>
type="button"
/>
</Box>
) : (
<div>
<Button
onClick={clickFileInput}
disabled={disabled || loading}
variant="secondary"
data-test-id="button-upload-file"
type="button"
>
{intl.formatMessage(commonMessages.chooseFile)}
</Button>
</div>
)}
{error && (
<Typography variant="caption" className={classes.errorText}>
<Text variant="caption" color="textCriticalDefault">
{helperText}
</Typography>
</Text>
)}
</div>
</Box>
<input
className={classes.fileField}
style={{ display: "none" }}
id="fileUpload"
onChange={event => onFileUpload(event.target.files[0])}
type="file"

View file

@ -1,32 +1,21 @@
import { makeStyles } from "@saleor/macaw-ui";
import { vars } from "@saleor/macaw-ui/next";
import clsx from "clsx";
import { Box } from "@saleor/macaw-ui/next";
import React from "react";
interface HrProps {
/**
* @deprecated use `Divider` component from `@saleor/macaw-ui/next`
*/
export const Hr: React.FC<{
className?: string;
}
const useStyles = makeStyles(
{
root: {
backgroundColor: vars.colors.border.neutralPlain,
border: "none",
display: "block",
height: 1,
margin: 0,
width: "100%",
},
},
{ name: "Hr" },
}> = ({ className }) => (
<Box
as="hr"
className={className}
backgroundColor="surfaceNeutralDepressed"
borderWidth={0}
width="100%"
height={1}
/>
);
export const Hr: React.FC<HrProps> = props => {
const { className } = props;
const classes = useStyles(props);
return <hr className={clsx(classes.root, className)} />;
};
Hr.displayName = "Hr";
export default Hr;

View file

@ -5,7 +5,7 @@ import userEvent from "@testing-library/user-event";
import React from "react";
import { props } from "./fixtures";
import Metadata from "./Metadata";
import { Metadata } from "./Metadata";
const Component = () => {
const { change, data } = useForm(props.data, jest.fn());

View file

@ -1,10 +1,10 @@
import { MetadataInput } from "@dashboard/graphql";
import { ChangeEvent } from "@dashboard/hooks/useForm";
import { removeAtIndex, updateAtIndex } from "@dashboard/utils/lists";
import { Box } from "@saleor/macaw-ui/next";
import React from "react";
import CardSpacer from "../CardSpacer";
import MetadataCard, { MetadataCardProps } from "./MetadataCard";
import { MetadataCard, MetadataCardProps } from "./MetadataCard";
import { EventDataAction, EventDataField } from "./types";
import { getDataKey, parseEventData } from "./utils";
@ -13,7 +13,7 @@ export interface MetadataProps
data: Record<"metadata" | "privateMetadata", MetadataInput[]>;
}
const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
export const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
const change = (event: ChangeEvent, isPrivate: boolean) => {
const { action, field, fieldIndex, value } = parseEventData(event);
const key = getDataKey(isPrivate);
@ -53,21 +53,17 @@ const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
};
return (
<>
<Box display="grid" gap={5} paddingBottom={9}>
<MetadataCard
data={data?.metadata}
isPrivate={false}
onChange={event => change(event, false)}
/>
<CardSpacer />
<MetadataCard
data={data?.privateMetadata}
isPrivate={true}
onChange={event => change(event, true)}
/>
</>
</Box>
);
};
Metadata.displayName = "Metadata";
export default Metadata;

View file

@ -1,27 +1,44 @@
import { Button } from "@dashboard/components/Button";
import TableRowLink from "@dashboard/components/TableRowLink";
import { MetadataInput } from "@dashboard/graphql";
import { FormChange } from "@dashboard/hooks/useForm";
import {
Card,
CardActions,
CardContent,
Table,
TableBody,
TableCell,
TableHead,
TextField,
Typography,
} from "@material-ui/core";
import { DeleteIcon, ExpandIcon, IconButton } from "@saleor/macaw-ui";
import clsx from "clsx";
import { makeStyles } from "@saleor/macaw-ui";
import {
Box,
Button,
ChervonDownIcon,
ChervonUpIcon,
Text,
TrashBinIcon,
vars,
} from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import CardTitle from "../CardTitle";
import { DashboardCard } from "../Card";
import Skeleton from "../Skeleton";
import useStyles from "./styles";
import { EventDataAction, EventDataField } from "./types";
import { EventDataAction } from "./types";
import { nameInputPrefix, nameSeparator, valueInputPrefix } from "./utils";
const useStyles = makeStyles(
theme => ({
input: {
padding: theme.spacing(0.5, 2),
},
nameInput: {
padding: "13px 16px",
},
}),
{
name: "Metadata",
},
);
export interface MetadataCardProps {
data: MetadataInput[];
@ -29,11 +46,7 @@ export interface MetadataCardProps {
onChange: FormChange;
}
export const nameSeparator = ":";
export const nameInputPrefix = EventDataField.name;
export const valueInputPrefix = EventDataField.value;
const MetadataCard: React.FC<MetadataCardProps> = ({
export const MetadataCard: React.FC<MetadataCardProps> = ({
data,
isPrivate,
onChange,
@ -43,15 +56,19 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
const classes = useStyles();
return (
<Card
<DashboardCard
data-test-id="metadata-editor"
data-test-is-private={isPrivate}
data-test-expanded={expanded}
gap={3}
>
<DashboardCard.Title>
<Box
display="flex"
justifyContent="space-between"
onClick={() => setExpanded(!expanded)}
cursor="pointer"
>
<CardTitle
className={classes.header}
title={
<>
{isPrivate
? intl.formatMessage({
id: "ETHnjq",
@ -63,29 +80,23 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
defaultMessage: "Metadata",
description: "header",
})}
<IconButton
className={clsx(classes.expandBtn, {
[classes.rotate]: expanded,
})}
hoverOutline={false}
<Button
variant="secondary"
data-test-id="expand"
onClick={() => setExpanded(!expanded)}
>
<ExpandIcon />
</IconButton>
</>
}
type="button"
icon={expanded ? <ChervonUpIcon /> : <ChervonDownIcon />}
/>
</Box>
</DashboardCard.Title>
{data === undefined ? (
<CardContent>
<DashboardCard.Content>
<Skeleton />
</CardContent>
</DashboardCard.Content>
) : (
<>
<CardContent className={classes.content}>
{data.length > 0 && (
<Typography color="textSecondary" variant="body2">
<DashboardCard.Content>
<Text variant="caption">
<FormattedMessage
id="2+v1wX"
defaultMessage="{number,plural,one{{number} string} other{{number} strings}}"
@ -94,52 +105,63 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
number: data.length,
}}
/>
</Typography>
</Text>
</DashboardCard.Content>
)}
</CardContent>
{expanded && (
<>
{data.length === 0 ? (
<CardContent className={classes.emptyContainer}>
<Typography variant="body2" color="textSecondary">
<DashboardCard.Content>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage
id="cY6H2C"
defaultMessage="No metadata created for this element. Use the button below to add new metadata field."
description="empty metadata text"
/>
</Typography>
</CardContent>
</Text>
</DashboardCard.Content>
) : (
<Table className={classes.table}>
<Table>
<TableHead>
<TableRowLink>
<TableCell className={classes.colNameHeader}>
<TableCell style={{ paddingLeft: vars.space[9] }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage
id="nudPsY"
defaultMessage="Field"
description="metadata field name, header"
/>
</Text>
</TableCell>
<TableCell className={classes.colValue}>
<TableCell style={{ paddingLeft: vars.space[11] }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage
id="LkuDEb"
defaultMessage="Value"
description="metadata field value, header"
/>
</Text>
</TableCell>
<TableCell className={classes.colActionHeader}>
<TableCell
style={{
textAlign: "end",
paddingRight: vars.space[9],
}}
>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage
id="nEixpu"
defaultMessage="Actions"
description="table action"
/>
</Text>
</TableCell>
</TableRowLink>
</TableHead>
<TableBody>
{data.map((field, fieldIndex) => (
<TableRowLink data-test-id="field" key={fieldIndex}>
<TableCell className={classes.colName}>
<TableCell style={{ paddingLeft: vars.space[9] }}>
<TextField
InputProps={{
classes: {
@ -155,7 +177,7 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
value={field.key}
/>
</TableCell>
<TableCell className={classes.colValue}>
<TableCell>
<TextField
InputProps={{
classes: {
@ -172,8 +194,9 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
value={field.value}
/>
</TableCell>
<TableCell className={classes.colAction}>
<IconButton
<TableCell style={{ paddingRight: vars.space[9] }}>
<Box display="flex" justifyContent="flex-end">
<Button
variant="secondary"
data-test-id={"delete-field-" + fieldIndex}
onClick={() =>
@ -184,19 +207,21 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
},
})
}
>
<DeleteIcon />
</IconButton>
type="button"
icon={<TrashBinIcon />}
/>
</Box>
</TableCell>
</TableRowLink>
))}
</TableBody>
</Table>
)}
<CardActions className={classes.actions}>
<DashboardCard.Content marginTop={5} paddingLeft={9}>
<Button
variant="secondary"
data-test-id="add-field"
type="button"
onClick={() =>
onChange({
target: {
@ -212,14 +237,11 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
description="add metadata field,button"
/>
</Button>
</CardActions>
</DashboardCard.Content>
</>
)}
</>
)}
</Card>
</DashboardCard>
);
};
MetadataCard.displayName = "MetadataCard";
export default MetadataCard;

View file

@ -1,5 +1,3 @@
export * from "./Metadata";
export * from "./MetadataCard";
export * from "./types";
export { default } from "./Metadata";
export { default as MetadataCard } from "./MetadataCard";

View file

@ -1,79 +0,0 @@
import { makeStyles } from "@saleor/macaw-ui";
const useStyles = makeStyles(
theme => {
const colAction: React.CSSProperties = {
textAlign: "right",
width: 130,
};
const colName: React.CSSProperties = {
width: 220,
};
return {
colAction: {
"&:last-child": {
...colAction,
paddingRight: theme.spacing(4),
},
},
colActionHeader: {
...colAction,
},
colName: {
...colName,
},
colNameHeader: {
...colName,
},
colValue: {},
actions: {
"&&": {
paddingBottom: theme.spacing(2),
paddingTop: theme.spacing(2),
paddingLeft: theme.spacing(4),
},
},
content: {
paddingBottom: 0,
paddingTop: theme.spacing(),
},
emptyContainer: {
paddingBottom: 0,
paddingTop: 0,
},
expandBtn: {
position: "relative",
left: theme.spacing(1),
top: -2,
transition: theme.transitions.create("transform", {
duration: theme.transitions.duration.shorter,
}),
border: 0,
},
header: {
"&&": {
paddingBottom: theme.spacing(1),
},
},
input: {
padding: theme.spacing(0.5, 2),
},
nameInput: {
padding: `13px 16px`,
},
table: {
marginTop: theme.spacing(2),
tableLayout: "fixed",
},
rotate: {
transform: "rotate(-180deg)",
},
};
},
{
name: "Metadata",
},
);
export default useStyles;

View file

@ -1,8 +1,11 @@
import { ChangeEvent } from "@dashboard/hooks/useForm";
import { nameSeparator } from "./MetadataCard";
import { EventData, EventDataAction, EventDataField } from "./types";
export const nameSeparator = ":";
export const nameInputPrefix = EventDataField.name;
export const valueInputPrefix = EventDataField.value;
export function parseEventData(event: ChangeEvent): EventData {
let action: EventDataAction;
let field: EventDataField = null;

View file

@ -38,11 +38,14 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
helperText,
editorRef,
onInitialize,
onChange,
...props
}) => {
const classes = useStyles({});
const id = useId(defaultId);
const [isFocused, setIsFocused] = React.useState(false);
const [hasValue, setHasValue] = React.useState(false);
const isTyped = Boolean(hasValue || isFocused);
const handleInitialize = React.useCallback((editor: EditorCore) => {
if (onInitialize) {
@ -68,7 +71,17 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
fullWidth
variant="outlined"
>
<InputLabel focused={true} shrink={true}>
<InputLabel
focused={true}
shrink={true}
classes={{
disabled: classes.labelDisabled,
error: classes.labelError,
root: classes.labelRoot,
}}
error={error}
disabled={disabled}
>
{label}
</InputLabel>
{hasRendered && (
@ -76,17 +89,25 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
// match with the id of holder div
holder={id}
tools={tools}
// LogLeves is undefined at runtime
// Log level is undefined at runtime
logLevel={"ERROR" as LogLevels.ERROR}
onInitialize={handleInitialize}
onChange={async event => {
const editorJsValue = await event.saver.save();
setHasValue(editorJsValue.blocks.length > 0);
return onChange?.();
}}
{...props}
>
<div
id={id}
className={clsx(classes.editor, classes.root, {
[classes.rootErrorFocus]: isFocused && error,
[classes.rootActive]: isFocused,
[classes.rootDisabled]: disabled,
[classes.rootError]: error,
[classes.rootTyped]:
isTyped || props.defaultValue?.blocks?.length > 0,
})}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}

View file

@ -1,16 +1,14 @@
import { alpha } from "@material-ui/core/styles";
import { makeStyles } from "@saleor/macaw-ui";
import { vars } from "@saleor/macaw-ui/next";
const useStyles = makeStyles(
theme => {
const hover = {
"&:hover": {
background: alpha(theme.palette.primary.main, 0.1),
background: vars.colors.background.interactiveNeutralHighlightHovering,
},
};
const isDarkMode = theme.palette.type === "dark";
return {
editor: {
"& .codex-editor": {
@ -20,17 +18,18 @@ const useStyles = makeStyles(
minHeight: 24,
},
"& .ce-block--selected .ce-block__content": {
background: `${alpha(theme.palette.primary.main, 0.2)} !important`,
background: `${vars.colors.background.interactiveNeutralHighlightPressing} !important`,
},
"& .ce-block__content": {
margin: 0,
maxWidth: "unset",
paddingRight: "54px",
},
"& .ce-conversion-tool": {
...hover,
},
"& .ce-conversion-tool--focused": {
background: `${alpha(theme.palette.primary.main, 0.1)} !important`,
background: `${vars.colors.background.interactiveNeutralHighlightHovering} !important`,
},
"& .ce-conversion-tool__icon": {
background: "none",
@ -77,13 +76,21 @@ const useStyles = makeStyles(
...hover,
},
"& .ce-popover": {
backgroundColor: theme.palette.background.paper,
backgroundColor: vars.colors.background.surfaceNeutralPlain,
position: "absolute",
top: 0,
left: "-186px",
},
"& .ce-settings": {
position: "absolute",
left: "-56px",
},
"& .ce-popover__item": {
...hover,
},
"& .ce-popover__item-icon": {
color: theme.palette.saleor.generic.verydark,
color: vars.colors.foreground.iconNeutralDefault,
backgroundColor: vars.colors.background.surfaceNeutralPlain,
},
"& .codex-editor__loader": {
@ -97,39 +104,62 @@ const useStyles = makeStyles(
paddingBottom: "0 !important",
},
"& a": {
color: theme.palette.primary.light,
color: vars.colors.foreground.textBrandDefault,
},
"&:not($rootDisabled):hover": {
borderColor: isDarkMode
? theme.palette.saleor.main[2]
: theme.palette.saleor.main[4],
"& .ce-popover__item--focused": {
background: `${vars.colors.background.interactiveNeutralHighlightHovering} !important`,
},
"& .cdx-search-field": {
backgroundColor: vars.colors.background.surfaceNeutralPlain,
},
},
root: {
border: `1px solid ${theme.palette.saleor.main[4]}`,
borderRadius: 4,
fontSize: theme.typography.body1.fontSize,
border: "1px solid transparent",
borderRadius: vars.borderRadius[3],
fontSize: vars.fontSize.bodyMedium,
backgroundColor: vars.colors.background.surfaceNeutralHighlight,
minHeight: 56,
padding: theme.spacing(3, 2),
paddingBottom: theme.spacing(),
paddingLeft: 10,
position: "relative",
transition: theme.transitions.duration.short + "ms",
padding: theme.spacing(3, 2),
paddingBottom: theme.spacing(),
paddingLeft: vars.space[4],
"&:hover": {
border: `1px solid ${vars.colors.border.neutralHighlight}`,
},
},
rootActive: {
borderColor: theme.palette.saleor.main[1],
border: `1px solid ${vars.colors.border.brandSubdued} !important`,
backgroundColor: `${vars.colors.background.interactiveNeutralHighlightDefault} !important`,
},
rootDisabled: {
...theme.overrides.MuiOutlinedInput.root["&$disabled"]["& fieldset"],
background: theme.palette.background.default,
color: theme.palette.saleor.main[4],
pointerEvents: "none",
backgroundColor: vars.colors.background.surfaceNeutralPlain,
border: `1px solid ${vars.colors.border.neutralHighlight}`,
color: vars.colors.foreground.textNeutralDisabled,
},
rootError: {
borderColor: theme.palette.error.main,
backgroundColor: vars.colors.background.surfaceCriticalSubdued,
},
rootStatic: {
fontSize: theme.typography.body1.fontSize,
},
labelRoot: {
marginLeft: "-6px",
color: `${vars.colors.foreground.textNeutralSubdued} !important`,
},
labelError: {
color: `${vars.colors.foreground.textCriticalSubdued} !important`,
},
rootErrorFocus: {
border: "1px solid transparent !important",
},
labelDisabled: {
color: `${vars.colors.foreground.textNeutralDisabled} !important`,
},
rootTyped: {
backgroundColor: vars.colors.background.surfaceNeutralPlain,
},
};
},
{ name: "RichTextEditor" },

View file

@ -1,4 +1,3 @@
import { Button } from "@dashboard/components/Button";
import {
CollectionErrorFragment,
PageErrorFragment,
@ -6,15 +5,13 @@ import {
} from "@dashboard/graphql";
import { getFieldError, getProductErrorMessage } from "@dashboard/utils/errors";
import getPageErrorMessage from "@dashboard/utils/errors/page";
import { Card, CardContent, TextField, Typography } from "@material-ui/core";
import { TextField } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import clsx from "clsx";
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
import slugify from "slugify";
import CardTitle from "../CardTitle";
import FormSpacer from "../FormSpacer";
import { DashboardCard } from "../Card";
enum SeoField {
slug = "slug",
@ -28,28 +25,7 @@ const maxTitleLength = 70;
const maxDescriptionLength = 300;
const useStyles = makeStyles(
theme => ({
addressBar: {
color: "#006621",
fontSize: "13px",
lineHeight: "16px",
marginBottom: "2px",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
},
container: {
width: "100%",
},
descriptionBar: {
color: "#545454",
fontSize: "13px",
lineHeight: "18px",
overflowWrap: "break-word",
},
helperText: {
marginBottom: theme.spacing(3),
},
{
label: {
flex: 1,
},
@ -59,21 +35,7 @@ const useStyles = makeStyles(
},
display: "flex",
},
preview: {
minHeight: theme.spacing(10),
},
title: {
padding: 0,
},
titleBar: {
color: "#1a0dab",
fontSize: "18px",
lineHeight: "21px",
overflowWrap: "break-word",
textDecoration: "none",
wordWrap: "break-word",
},
}),
{ name: "SeoForm" },
);
@ -95,7 +57,7 @@ interface SeoFormProps {
onClick?();
}
const SeoForm: React.FC<SeoFormProps> = props => {
export const SeoForm: React.FC<SeoFormProps> = props => {
const {
description,
descriptionPlaceholder,
@ -157,17 +119,18 @@ const SeoForm: React.FC<SeoFormProps> = props => {
const getError = (fieldName: SeoField) => getFieldError(errors, fieldName);
return (
<Card>
<CardTitle
title={intl.formatMessage({
id: "TGX4T1",
defaultMessage: "Search Engine Preview",
})}
toolbar={
<DashboardCard>
<DashboardCard.Title>
<Box display="flex" justifyContent="space-between" alignItems="center">
<FormattedMessage
defaultMessage="Search Engine Preview"
id="TGX4T1"
/>
<Button
variant="tertiary"
variant="secondary"
onClick={toggleExpansion}
data-test-id="edit-seo"
type="button"
>
<FormattedMessage
id="s5Imt5"
@ -175,91 +138,77 @@ const SeoForm: React.FC<SeoFormProps> = props => {
description="button"
/>
</Button>
}
/>
<CardContent>
{shouldDisplayHelperText && (
<Typography className={clsx({ [classes.helperText]: expanded })}>
{helperText}
</Typography>
)}
</Box>
</DashboardCard.Title>
<DashboardCard.Content>
{shouldDisplayHelperText && <Text>{helperText}</Text>}
{expanded && (
<div className={classes.container}>
<TextField
<Box display="grid" gap={5}>
<Box>
<Input
error={!!getError(SeoField.slug) || slug.length > maxSlugLength}
name={SeoField.slug}
label={
<div className={classes.labelContainer}>
<div className={classes.label}>
<FormattedMessage id="IoDlcd" defaultMessage="Slug" />
</div>
<Box display="flex" gap={3}>
<Box as="span">
<FormattedMessage defaultMessage="Slug" id="IoDlcd" />
</Box>
{slug?.length > 0 && (
<span>
<Box as="span">
<FormattedMessage
id="ChAjJu"
defaultMessage="{numberOfCharacters} of {maxCharacters} characters"
description="character limit"
defaultMessage="({numberOfCharacters} of {maxCharacters} characters)"
id="yi1HSj"
values={{
maxCharacters: maxSlugLength,
numberOfCharacters: slug?.length,
}}
/>
</span>
</Box>
)}
</div>
</Box>
}
InputProps={{
inputProps: {
maxLength: maxSlugLength,
},
}}
helperText={getSlugHelperMessage()}
size="small"
value={slug}
disabled={loading || disabled}
placeholder={slug || slugify(slugPlaceholder, { lower: true })}
onChange={handleSlugChange}
fullWidth
disabled={loading || disabled}
maxLength={maxSlugLength}
placeholder={slugPlaceholder}
/>
<FormSpacer />
<TextField
</Box>
<Input
size="small"
error={title?.length > maxTitleLength}
name={SeoField.title}
label={
<div className={classes.labelContainer}>
<div className={classes.label}>
<FormattedMessage
id="w2Cewo"
defaultMessage="Search engine title"
/>
</div>
{title?.length > 0 && (
<span>
<FormattedMessage
id="ChAjJu"
defaultMessage="{numberOfCharacters} of {maxCharacters} characters"
description="character limit"
values={{
maxCharacters: maxTitleLength,
numberOfCharacters: title.length,
}}
/>
</span>
)}
</div>
}
InputProps={{
inputProps: {
maxLength: maxTitleLength,
},
}}
helperText={intl.formatMessage(seoFieldMessage)}
value={title ?? ""}
disabled={loading || disabled}
placeholder={titlePlaceholder}
onChange={onChange}
fullWidth
maxLength={maxTitleLength}
placeholder={titlePlaceholder}
helperText={intl.formatMessage(seoFieldMessage)}
label={
<Box display="flex" gap={3}>
<Box as="span">
<FormattedMessage
defaultMessage="Search engine title"
id="w2Cewo"
/>
</Box>
{title?.length > 0 && (
<Box as="span">
<FormattedMessage
defaultMessage="({numberOfCharacters} of {maxCharacters} characters)"
id="yi1HSj"
values={{
maxCharacters: maxTitleLength,
numberOfCharacters: title?.length,
}}
/>
</Box>
)}
</Box>
}
/>
<FormSpacer />
<TextField
error={description?.length > maxDescriptionLength}
name={SeoField.description}
@ -300,11 +249,9 @@ const SeoForm: React.FC<SeoFormProps> = props => {
placeholder={descriptionPlaceholder}
rows={10}
/>
</div>
</Box>
)}
</CardContent>
</Card>
</DashboardCard.Content>
</DashboardCard>
);
};
SeoForm.displayName = "SeoForm";
export default SeoForm;

View file

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

View file

@ -45,13 +45,15 @@ export interface SingleAutocompleteSelectFieldProps
nakedInput?: boolean;
onBlur?: () => void;
popperPlacement?: PopperPlacementType;
id?: string;
}
const DebounceAutocomplete: React.ComponentType<DebounceProps<
string
>> = Debounce;
const DebounceAutocomplete: React.ComponentType<DebounceProps<string>> =
Debounce;
const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectFieldProps> = props => {
const SingleAutocompleteSelectFieldComponent: React.FC<
SingleAutocompleteSelectFieldProps
> = props => {
const {
add,
allowCustomValues,
@ -78,6 +80,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
nakedInput = false,
onBlur,
popperPlacement = "bottom-end",
id,
...rest
} = props;
const classes = useStyles(props);
@ -243,6 +246,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
fullWidth={true}
onBlur={onBlur}
inputRef={input}
id={id}
/>
{isOpen && (!!inputValue || !!choices.length) && (
<Popper
@ -284,11 +288,9 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
);
};
const SingleAutocompleteSelectField: React.FC<SingleAutocompleteSelectFieldProps> = ({
choices,
fetchChoices,
...rest
}) => {
const SingleAutocompleteSelectField: React.FC<
SingleAutocompleteSelectFieldProps
> = ({ choices, fetchChoices, ...rest }) => {
const [query, setQuery] = React.useState("");
if (fetchChoices) {

View file

@ -1 +0,0 @@
export * from "./TopNavMenu";

View file

@ -9,7 +9,7 @@ import CardMenu from "@dashboard/components/CardMenu/CardMenu";
import { CardSpacer } from "@dashboard/components/CardSpacer";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata/Metadata";
import { Metadata } from "@dashboard/components/Metadata/Metadata";
import { MetadataFormData } from "@dashboard/components/Metadata/types";
import RequirePermissions from "@dashboard/components/RequirePermissions";
import Savebar from "@dashboard/components/Savebar";

View file

@ -3,7 +3,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import { createSaleChannelsChangeHandler } from "@dashboard/discounts/handlers";
import { saleListUrl } from "@dashboard/discounts/urls";

View file

@ -4,7 +4,7 @@ import CardSpacer from "@dashboard/components/CardSpacer";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import { Tab, TabContainer } from "@dashboard/components/Tab";
import {

View file

@ -3,7 +3,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata";
import { Metadata } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import {
createChannelsChangeHandler,

View file

@ -5,7 +5,7 @@ import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailability
import CountryList from "@dashboard/components/CountryList";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import { Tab, TabContainer } from "@dashboard/components/Tab";
import {

View file

@ -1,6 +1,6 @@
import CardSpacer from "@dashboard/components/CardSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata";
import { Metadata } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import useNavigator from "@dashboard/hooks/useNavigator";
import React from "react";

View file

@ -9,7 +9,7 @@ import { CardSpacer } from "@dashboard/components/CardSpacer";
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import {
OrderDetailsFragment,

View file

@ -1,7 +1,6 @@
import { Button } from "@dashboard/components/Button";
import CardTitle from "@dashboard/components/CardTitle";
import HorizontalSpacer from "@dashboard/components/HorizontalSpacer";
import { Hr } from "@dashboard/components/Hr";
import Money from "@dashboard/components/Money";
import { Pill } from "@dashboard/components/Pill";
import Skeleton from "@dashboard/components/Skeleton";
@ -12,6 +11,7 @@ import {
OrderStatus,
} from "@dashboard/graphql";
import { Card, CardContent } from "@material-ui/core";
import { Divider } from "@saleor/macaw-ui/next";
import clsx from "clsx";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -201,7 +201,7 @@ const OrderPayment: React.FC<OrderPaymentProps> = props => {
</div>
</div>
</CardContent>
<Hr />
<Divider />
<CardContent className={classes.payments}>
<div className={classes.root}>
{!!usedGiftCardAmount && (

View file

@ -31,6 +31,7 @@ jest.mock("@saleor/macaw-ui", () => ({
jest.mock("@saleor/macaw-ui/next", () => ({
useTheme: jest.fn(() => () => ({})),
Divider: jest.fn(() => <></>),
vars: {
colors: {
border: {

View file

@ -1,6 +1,5 @@
import { Button } from "@dashboard/components/Button";
import CardTitle from "@dashboard/components/CardTitle";
import { Hr } from "@dashboard/components/Hr";
import { Pill } from "@dashboard/components/Pill";
import Skeleton from "@dashboard/components/Skeleton";
import { OrderAction, OrderDetailsFragment } from "@dashboard/graphql";
@ -10,6 +9,7 @@ import {
orderSendRefundUrl,
} from "@dashboard/orders/urls";
import { Card, CardContent, Typography } from "@material-ui/core";
import { Divider } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -94,7 +94,7 @@ const OrderPaymentSummaryCard: React.FC<OrderPaymementProps> = ({
<PaymentsSummary order={order} />
{canAnyRefund && (
<>
<Hr />
<Divider />
<CardTitle
toolbar={
<div className={classes.refundsButtons}>

View file

@ -3,7 +3,7 @@ import Form from "@dashboard/components/Form";
import Grid from "@dashboard/components/Grid";
import Hr from "@dashboard/components/Hr";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import { PageErrorFragment } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator";

View file

@ -3,7 +3,7 @@ import Form from "@dashboard/components/Form";
import Grid from "@dashboard/components/Grid";
import Hr from "@dashboard/components/Hr";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata/Metadata";
import { Metadata } from "@dashboard/components/Metadata";
import { MetadataFormData } from "@dashboard/components/Metadata/types";
import Savebar from "@dashboard/components/Savebar";
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";

View file

@ -4,12 +4,12 @@ import {
} from "@dashboard/attributes/utils/data";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
import Attributes, { AttributeInput } from "@dashboard/components/Attributes";
import { AttributeInput, Attributes } from "@dashboard/components/Attributes";
import CardSpacer from "@dashboard/components/CardSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata";
import { Metadata } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm";
import { SeoForm } from "@dashboard/components/SeoForm";
import VisibilityCard from "@dashboard/components/VisibilityCard";
import {
PageDetailsFragment,

View file

@ -2,7 +2,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import CardSpacer from "@dashboard/components/CardSpacer";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import {
ProductTypeKindEnum,

View file

@ -3,7 +3,7 @@ import CardSpacer from "@dashboard/components/CardSpacer";
import ControlledSwitch from "@dashboard/components/ControlledSwitch";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata/Metadata";
import { Metadata } from "@dashboard/components/Metadata/Metadata";
import { MetadataFormData } from "@dashboard/components/Metadata/types";
import Savebar from "@dashboard/components/Savebar";
import {

View file

@ -6,14 +6,14 @@ import CannotDefineChannelsAvailabilityCard from "@dashboard/channels/components
import { ChannelData } from "@dashboard/channels/utils";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
import Attributes, { AttributeInput } from "@dashboard/components/Attributes";
import { AttributeInput, Attributes } from "@dashboard/components/Attributes";
import CardSpacer from "@dashboard/components/CardSpacer";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata";
import { Metadata } from "@dashboard/components/Metadata";
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
import Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm";
import { SeoForm } from "@dashboard/components/SeoForm";
import {
PermissionEnum,
ProductChannelListingErrorFragment,
@ -30,7 +30,7 @@ import {
} from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator";
import useStateFromProps from "@dashboard/hooks/useStateFromProps";
import ProductVariantPrice from "@dashboard/products/components/ProductVariantPrice";
import { ProductVariantPrice } from "@dashboard/products/components/ProductVariantPrice";
import {
ProductCreateUrlQueryParams,
productListUrl,
@ -41,10 +41,10 @@ import React from "react";
import { useIntl } from "react-intl";
import { FetchMoreProps, RelayToFlat } from "../../../types";
import ProductDetailsForm from "../ProductDetailsForm";
import ProductOrganization from "../ProductOrganization";
import ProductShipping from "../ProductShipping/ProductShipping";
import ProductStocks from "../ProductStocks";
import { ProductDetailsForm } from "../ProductDetailsForm";
import { ProductOrganization } from "../ProductOrganization";
import { ProductShipping } from "../ProductShipping";
import { ProductStocks } from "../ProductStocks";
import ProductTaxes from "../ProductTaxes";
import ProductCreateForm, {
ProductCreateData,
@ -263,7 +263,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
richTextGetters={attributeRichTextGetters}
/>
)}
<CardSpacer />
{isSimpleProduct && (
<>
<ProductShipping
@ -273,14 +272,12 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
weightUnit={weightUnit}
onChange={change}
/>
<CardSpacer />
<ProductVariantPrice
ProductVariantChannelListings={data.channelListings}
errors={channelsErrors}
loading={loading}
onChange={handlers.changeChannelPrice}
/>
<CardSpacer />
<ProductStocks
data={data}
disabled={loading}
@ -315,7 +312,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
loading={loading}
onChange={change}
/>
<CardSpacer />
<Metadata data={data} onChange={handlers.changeMetadata} />
</DetailPageLayout.Content>
<DetailPageLayout.RightSidebar>
@ -341,7 +337,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
onProductTypeChange={handlers.selectProductType}
collectionsInputDisplayValue={selectedCollections}
/>
<CardSpacer />
{isSimpleProduct ? (
<ChannelsAvailabilityCard
managePermissions={[PermissionEnum.MANAGE_PRODUCTS]}

View file

@ -1,7 +1,4 @@
import CardTitle from "@dashboard/components/CardTitle";
import FormSpacer from "@dashboard/components/FormSpacer";
import Grid from "@dashboard/components/Grid";
import Hr from "@dashboard/components/Hr";
import { DashboardCard } from "@dashboard/components/Card";
import RichTextEditor from "@dashboard/components/RichTextEditor";
import { RichTextEditorLoading } from "@dashboard/components/RichTextEditor/RichTextEditorLoading";
import { ProductErrorFragment } from "@dashboard/graphql";
@ -9,7 +6,7 @@ import { commonMessages } from "@dashboard/intl";
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
import { useRichTextContext } from "@dashboard/utils/richText/context";
import { OutputData } from "@editorjs/editorjs";
import { Card, CardContent, TextField } from "@material-ui/core";
import { Box, Input } from "@saleor/macaw-ui/next";
import React from "react";
import { useIntl } from "react-intl";
@ -27,36 +24,36 @@ interface ProductDetailsFormProps {
export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
data,
disabled,
errors,
onChange,
errors,
disabled,
}) => {
const intl = useIntl();
const formErrors = getFormErrors(["name", "description", "rating"], errors);
const { editorRef, defaultValue, isReadyForMount, handleChange } =
useRichTextContext();
const formErrors = getFormErrors(["name", "description", "rating"], errors);
return (
<Card>
<CardTitle
title={intl.formatMessage(commonMessages.generalInformations)}
/>
<CardContent>
<TextField
error={!!formErrors.name}
helperText={getProductErrorMessage(formErrors.name, intl)}
fullWidth
<DashboardCard>
<DashboardCard.Title>
{intl.formatMessage(commonMessages.generalInformations)}
</DashboardCard.Title>
<DashboardCard.Content display="grid" gap={5} paddingX={8}>
<Input
label={intl.formatMessage({
id: "6AMFki",
defaultMessage: "Name",
description: "product name",
})}
name="name"
value={data.name}
size="small"
value={data.name || ""}
onChange={onChange}
error={!!formErrors.name}
name="name"
disabled={disabled}
helperText={getProductErrorMessage(formErrors.name, intl)}
/>
<FormSpacer />
{isReadyForMount ? (
<RichTextEditor
editorRef={editorRef}
@ -74,27 +71,24 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
name="description"
/>
)}
<FormSpacer />
<Hr />
<FormSpacer />
<Grid variant="uniform">
<TextField
type="number"
error={!!formErrors.rating}
helperText={getProductErrorMessage(formErrors.rating, intl)}
disabled={disabled}
<Box __width="25%">
<Input
label={intl.formatMessage({
id: "L7N+0y",
defaultMessage: "Product Rating",
description: "product rating",
})}
name="rating"
size="small"
value={data.rating || ""}
onChange={onChange}
error={!!formErrors.rating}
name="rating"
type="number"
disabled={disabled}
helperText={getProductErrorMessage(formErrors.rating, intl)}
/>
</Grid>
</CardContent>
</Card>
</Box>
</DashboardCard.Content>
</DashboardCard>
);
};
export default ProductDetailsForm;

View file

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

View file

@ -1,17 +1,15 @@
import BackButton from "@dashboard/components/BackButton";
import { Button } from "@dashboard/components/Button";
import Form from "@dashboard/components/Form";
import FormSpacer from "@dashboard/components/FormSpacer";
import { ProductFragment } from "@dashboard/graphql";
import { SubmitPromise } from "@dashboard/hooks/useForm";
import { buttonMessages } from "@dashboard/intl";
import {
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
Typography,
} from "@material-ui/core";
import { Button, Input, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
@ -52,33 +50,37 @@ const ProductExternalMediaDialog: React.FC<ProductExternalMediaDialogProps> = ({
return (
<Dialog onClose={onClose} open={open}>
<DialogTitle disableTypography>
<Text variant="heading">
{intl.formatMessage(messages.buttonMessage)}
</Text>
</DialogTitle>
<Form initial={initialValues} onSubmit={handleOnSubmit}>
{({ change, data, submit }) => (
<>
<DialogContent>
<Typography>
<Text variant="caption">
<FormattedMessage
id="zDvDnG"
defaultMessage="Media from the URL you supply will be shown in the media gallery. You will be able to define the order of the gallery."
description="modal header"
/>
</Typography>
</Text>
<FormSpacer />
<TextField
<Input
label="URL"
value={data.mediaUrl}
name="mediaUrl"
type="url"
type="text"
onChange={change}
autoFocus
fullWidth
size="medium"
/>
</DialogContent>
<DialogActions>
<BackButton onClick={onClose} />
<Button variant="secondary" onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<Button onClick={submit}>
{intl.formatMessage(messages.buttonMessage)}
</Button>

View file

@ -11,7 +11,6 @@ import { getByName } from "@dashboard/components/Filter/utils";
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
import { ListPageLayout } from "@dashboard/components/Layouts";
import LimitReachedAlert from "@dashboard/components/LimitReachedAlert";
import { TopNavMenu } from "@dashboard/components/TopNavMenu";
import { ProductListColumns } from "@dashboard/config";
import {
GridAttributesQuery,
@ -188,7 +187,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
)}
</Text>
)}
<TopNavMenu
<TopNav.Menu
dataTestId="menu"
items={[
{

View file

@ -1,121 +1,23 @@
import { Button } from "@dashboard/components/Button";
import CardTitle from "@dashboard/components/CardTitle";
import { DashboardCard } from "@dashboard/components/Card";
import ImageUpload from "@dashboard/components/ImageUpload";
import MediaTile from "@dashboard/components/MediaTile";
import Skeleton from "@dashboard/components/Skeleton";
import { ProductMediaFragment, ProductMediaType } from "@dashboard/graphql";
import { ProductMediaPopper } from "@dashboard/products/components/ProductMediaPopper/ProductMediaPopper";
import { ReorderAction } from "@dashboard/types";
import createMultiFileUploadHandler from "@dashboard/utils/handlers/multiFileUploadHandler";
import { Card, CardContent } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import { vars } from "@saleor/macaw-ui/next";
import clsx from "clsx";
import {
Box,
Button,
Dropdown,
List,
sprinkles,
Text,
} from "@saleor/macaw-ui/next";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
import { SortableContainer, SortableElement } from "react-sortable-hoc";
const messages = defineMessages({
media: {
id: "/Mcvt4",
defaultMessage: "Media",
description: "section header",
},
upload: {
id: "mGiA6q",
defaultMessage: "Upload",
description: "modal button upload",
},
});
const useStyles = makeStyles(
theme => ({
card: {
marginTop: theme.spacing(2),
[theme.breakpoints.down("sm")]: {
marginTop: 0,
},
},
fileField: {
display: "none",
},
icon: {
color: "rgba(255, 255, 255, 0.54)",
},
image: {
height: "100%",
objectFit: "contain",
userSelect: "none",
width: "100%",
},
imageContainer: {
"&:hover, &.dragged": {
"& $imageOverlay": {
display: "block",
},
},
background: "#ffffff",
border: `1px solid ${vars.colors.border.neutralPlain}`,
borderRadius: theme.spacing(),
height: 140,
margin: "auto",
overflow: "hidden",
padding: theme.spacing(2),
position: "relative",
width: 140,
},
imageGridContainer: {
position: "relative",
},
imageOverlay: {
background: "rgba(0, 0, 0, 0.6)",
cursor: "move",
display: "none",
height: 140,
left: 0,
padding: theme.spacing(2),
position: "absolute",
top: 0,
width: 140,
},
imageOverlayToolbar: {
alignContent: "flex-end",
display: "flex",
position: "relative",
right: theme.spacing(-3),
top: theme.spacing(-2),
},
imageUpload: {
height: "100%",
left: 0,
outline: 0,
position: "absolute",
top: 0,
width: "100%",
},
imageUploadActive: {
zIndex: 1,
},
imageUploadIconActive: {
display: "block",
},
root: {
display: "grid",
gridColumnGap: theme.spacing(2),
gridRowGap: theme.spacing(2),
gridTemplateColumns: "repeat(4, 1fr)",
[theme.breakpoints.down("sm")]: {
gridTemplateColumns: "repeat(3, 1fr)",
},
[theme.breakpoints.down("xs")]: {
gridTemplateColumns: "repeat(2, 1fr)",
},
},
rootDragActive: {
opacity: 0.2,
},
}),
{ name: "ProductMedia" },
);
import { messages } from "./messages";
interface SortableMediaProps {
media: {
@ -163,7 +65,6 @@ const MediaListContainer = SortableContainer<MediaListContainerProps>(
);
interface ProductMediaProps {
placeholderImage?: string;
media: ProductMediaFragment[];
loading?: boolean;
getImageEditUrl: (id: string) => string;
@ -176,7 +77,6 @@ interface ProductMediaProps {
const ProductMedia: React.FC<ProductMediaProps> = props => {
const {
media,
placeholderImage,
getImageEditUrl,
onImageDelete,
onImageReorder,
@ -184,14 +84,12 @@ const ProductMedia: React.FC<ProductMediaProps> = props => {
openMediaUrlModal,
} = props;
const classes = useStyles(props);
const intl = useIntl();
const imagesUpload = React.useRef<HTMLInputElement>(null);
const anchor = React.useRef<HTMLButtonElement>();
const [imagesToUpload, setImagesToUpload] = React.useState<
ProductMediaFragment[]
>([]);
const [popperOpenStatus, setPopperOpenStatus] = React.useState(false);
const handleImageUpload = createMultiFileUploadHandler(onImageUpload, {
onAfterUpload: () =>
@ -219,61 +117,92 @@ const ProductMedia: React.FC<ProductMediaProps> = props => {
});
return (
<Card className={classes.card}>
<CardTitle
title={intl.formatMessage(messages.media)}
toolbar={
<>
<DashboardCard>
<DashboardCard.Title>
<Box display="flex" justifyContent="space-between" cursor="pointer">
<FormattedMessage {...messages.media} />
<Dropdown>
<Dropdown.Trigger>
<Button
onClick={() => setPopperOpenStatus(true)}
variant="tertiary"
variant="secondary"
type="button"
data-test-id="button-upload-image"
ref={anchor}
>
{intl.formatMessage(messages.upload)}
</Button>
<ProductMediaPopper
anchorRef={anchor.current}
imagesUploadRef={imagesUpload.current}
setPopperStatus={setPopperOpenStatus}
popperStatus={popperOpenStatus}
openMediaUrlModal={openMediaUrlModal}
/>
<input
className={classes.fileField}
</Dropdown.Trigger>
<Dropdown.Content align="end">
<List
padding={5}
borderRadius={4}
boxShadow="overlay"
backgroundColor="surfaceNeutralPlain"
>
<Dropdown.Item>
<List.Item
borderRadius={4}
paddingX={4}
paddingY={5}
onClick={() => imagesUpload.current.click()}
data-test-id="upload-images"
>
<Text>{intl.formatMessage(messages.uploadImages)}</Text>
</List.Item>
</Dropdown.Item>
<Dropdown.Item>
<List.Item
borderRadius={4}
paddingX={4}
paddingY={5}
onClick={openMediaUrlModal}
data-test-id="upload-media-url"
>
<Text>{intl.formatMessage(messages.uploadUrl)}</Text>
</List.Item>
</Dropdown.Item>
</List>
</Dropdown.Content>
</Dropdown>
</Box>
</DashboardCard.Title>
<DashboardCard.Content>
<Box>
<Box
as="input"
display="none"
id="fileUpload"
onChange={event => handleImageUpload(event.target.files)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
handleImageUpload(event.target.files)
}
multiple
type="file"
ref={imagesUpload}
accept="image/*"
/>
</>
}
/>
<div className={classes.imageGridContainer}>
</Box>
<Box position="relative">
{media === undefined ? (
<CardContent>
<div className={classes.root}>
<div className={classes.imageContainer}>
<img className={classes.image} src={placeholderImage} />
</div>
</div>
</CardContent>
<Box padding={8}>
<Skeleton />
</Box>
) : media.length > 0 ? (
<>
<ImageUpload
className={classes.imageUpload}
isActiveClassName={classes.imageUploadActive}
className={sprinkles({
height: "100%",
width: "100%",
position: "absolute",
top: 0,
left: 0,
})}
isActiveClassName={sprinkles({ zIndex: "1" })}
disableClick={true}
hideUploadIcon={true}
iconContainerActiveClassName={classes.imageUploadIconActive}
iconContainerActiveClassName={sprinkles({ display: "block" })}
onImageUpload={handleImageUpload}
>
{({ isDragActive }) => (
<CardContent>
<MediaListContainer
distance={20}
helperClass="dragged"
@ -281,22 +210,24 @@ const ProductMedia: React.FC<ProductMediaProps> = props => {
media={media}
preview={imagesToUpload}
onSortEnd={onImageReorder}
className={clsx({
[classes.root]: true,
[classes.rootDragActive]: isDragActive,
className={sprinkles({
display: "grid",
gap: 8,
gridTemplateColumns: { mobile: 2, tablet: 3, desktop: 4 },
opacity: isDragActive ? "0.2" : "1",
})}
onDelete={onImageDelete}
getEditHref={getImageEditUrl}
/>
</CardContent>
)}
</ImageUpload>
</>
) : (
<ImageUpload onImageUpload={handleImageUpload} />
)}
</div>
</Card>
</Box>
</DashboardCard.Content>
</DashboardCard>
);
};
ProductMedia.displayName = "ProductMedia";

View file

@ -0,0 +1,24 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
media: {
id: "/Mcvt4",
defaultMessage: "Media",
description: "section header",
},
upload: {
id: "mGiA6q",
defaultMessage: "Upload",
description: "modal button upload",
},
uploadImages: {
id: "9CEu8k",
defaultMessage: "Upload Images",
description: "modal button images upload",
},
uploadUrl: {
id: "Q2UXlW",
defaultMessage: "Upload URL",
description: "modal button url upload",
},
});

View file

@ -1,78 +0,0 @@
import {
ClickAwayListener,
Grow,
MenuItem,
MenuList as Menu,
Paper,
Popper,
} from "@material-ui/core";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
interface ProductMediaPopperProps {
anchorRef: HTMLButtonElement;
imagesUploadRef: HTMLInputElement;
openMediaUrlModal: () => void;
popperStatus: boolean;
setPopperStatus: (popperStatus: boolean) => void;
}
const messages = defineMessages({
uploadImages: {
id: "9CEu8k",
defaultMessage: "Upload Images",
description: "modal button images upload",
},
uploadUrl: {
id: "Q2UXlW",
defaultMessage: "Upload URL",
description: "modal button url upload",
},
});
export const ProductMediaPopper = ({
anchorRef,
imagesUploadRef,
setPopperStatus,
openMediaUrlModal,
popperStatus,
}: ProductMediaPopperProps) => {
const intl = useIntl();
return (
<Popper
open={popperStatus}
anchorEl={anchorRef}
transition
placement="bottom-end"
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<Paper>
<ClickAwayListener
onClickAway={() => setPopperStatus(false)}
mouseEvent="onClick"
>
<Menu>
<MenuItem
onClick={() => imagesUploadRef.click()}
data-test-id="upload-images"
key="upload-images"
>
{intl.formatMessage(messages.uploadImages)}
</MenuItem>
<MenuItem
onClick={openMediaUrlModal}
data-test-id="upload-media-url"
key="upload-media-url"
>
{intl.formatMessage(messages.uploadUrl)}
</MenuItem>
</Menu>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
);
};

View file

@ -1,7 +1,4 @@
import CardSpacer from "@dashboard/components/CardSpacer";
import CardTitle from "@dashboard/components/CardTitle";
import { FormSpacer } from "@dashboard/components/FormSpacer";
import Hr from "@dashboard/components/Hr";
import { DashboardCard } from "@dashboard/components/Card";
import Link from "@dashboard/components/Link";
import MultiAutocompleteSelectField, {
MultiAutocompleteChoiceType,
@ -16,12 +13,10 @@ import {
} from "@dashboard/graphql";
import { ChangeEvent } from "@dashboard/hooks/useForm";
import { commonMessages } from "@dashboard/intl";
import { maybe } from "@dashboard/misc";
import { productTypeUrl } from "@dashboard/productTypes/urls";
import { FetchMoreProps } from "@dashboard/types";
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
import { Card, CardContent, Typography } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import { Box, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -31,22 +26,6 @@ interface ProductType {
name: string;
}
const useStyles = makeStyles(
theme => ({
card: {
overflow: "visible",
},
cardSubtitle: {
fontSize: theme.typography.body1.fontSize,
marginBottom: theme.spacing(0.5),
},
label: {
marginBottom: theme.spacing(0.5),
},
}),
{ name: "ProductOrganization" },
);
interface ProductOrganizationProps {
canChangeType: boolean;
categories?: SingleAutocompleteChoiceType[];
@ -74,7 +53,9 @@ interface ProductOrganizationProps {
onProductTypeChange?: (event: ChangeEvent) => void;
}
const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
export const ProductOrganization: React.FC<
ProductOrganizationProps
> = props => {
const {
canChangeType,
categories,
@ -98,7 +79,6 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
onProductTypeChange,
} = props;
const classes = useStyles(props);
const intl = useIntl();
const formErrors = getFormErrors(
@ -111,15 +91,15 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
: null;
return (
<Card className={classes.card}>
<CardTitle
title={intl.formatMessage({
<DashboardCard>
<DashboardCard.Title>
{intl.formatMessage({
id: "JjeZEG",
defaultMessage: "Organize Product",
description: "section header",
})}
/>
<CardContent>
</DashboardCard.Title>
<DashboardCard.Content gap={8} display="flex" flexDirection="column">
{canChangeType ? (
<SingleAutocompleteSelectField
displayValue={productTypeInputDisplayValue}
@ -139,36 +119,33 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
{...fetchMoreProductTypes}
/>
) : (
<>
<Typography className={classes.label} variant="caption">
<Box display="flex" flexDirection="column" gap={6}>
<Box display="flex" flexDirection="column">
<Text variant="bodyEmp">
<FormattedMessage id="anK7jD" defaultMessage="Product Type" />
</Typography>
<Typography>
</Text>
<Text variant="caption">
<Link
href={productTypeUrl(productType?.id) ?? ""}
disabled={!productType?.id}
>
{productType?.name ?? "..."}
</Link>
</Typography>
<CardSpacer />
<Typography className={classes.label} variant="caption">
</Text>
</Box>
<Box display="flex" flexDirection="column">
<Text variant="bodyEmp">
<FormattedMessage id="Be+J13" defaultMessage="Configurable" />
</Typography>
<Typography>
{maybe(
() =>
productType.hasVariants
</Text>
<Text variant="caption">
{productType?.hasVariants
? intl.formatMessage(commonMessages.yes)
: intl.formatMessage(commonMessages.no),
"...",
: intl.formatMessage(commonMessages.no)}
</Text>
</Box>
</Box>
)}
</Typography>
</>
)}
<FormSpacer />
<Hr />
<FormSpacer />
<SingleAutocompleteSelectField
displayValue={categoryInputDisplayValue}
error={!!(formErrors.category || noCategoryError)}
@ -189,9 +166,6 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
data-test-id="category"
{...fetchMoreCategories}
/>
<FormSpacer />
<Hr />
<FormSpacer />
<MultiAutocompleteSelectField
displayValues={collectionsInputDisplayValue}
error={!!formErrors.collections}
@ -217,9 +191,7 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
testId="collection"
{...fetchMoreCollections}
/>
</CardContent>
</Card>
</DashboardCard.Content>
</DashboardCard>
);
};
ProductOrganization.displayName = "ProductOrganization";
export default ProductOrganization;

View file

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

View file

@ -1,14 +1,8 @@
import CardTitle from "@dashboard/components/CardTitle";
import Grid from "@dashboard/components/Grid";
import { DashboardCard } from "@dashboard/components/Card";
import { ProductErrorFragment } from "@dashboard/graphql";
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
import createNonNegativeValueChangeHandler from "@dashboard/utils/handlers/nonNegativeValueChangeHandler";
import {
Card,
CardContent,
InputAdornment,
TextField,
} from "@material-ui/core";
import { InputAdornment, TextField } from "@material-ui/core";
import React from "react";
import { useIntl } from "react-intl";
@ -22,7 +16,7 @@ interface ProductShippingProps {
onChange: (event: React.ChangeEvent<any>) => void;
}
const ProductShipping: React.FC<ProductShippingProps> = props => {
export const ProductShipping: React.FC<ProductShippingProps> = props => {
const { data, disabled, errors, weightUnit, onChange } = props;
const intl = useIntl();
@ -31,16 +25,15 @@ const ProductShipping: React.FC<ProductShippingProps> = props => {
const handleChange = createNonNegativeValueChangeHandler(onChange);
return (
<Card>
<CardTitle
title={intl.formatMessage({
<DashboardCard>
<DashboardCard.Title>
{intl.formatMessage({
id: "3rIMq/",
defaultMessage: "Shipping",
description: "product shipping",
})}
/>
<CardContent>
<Grid variant="uniform">
</DashboardCard.Title>
<DashboardCard.Content>
<TextField
disabled={disabled}
label={intl.formatMessage({
@ -51,23 +44,19 @@ const ProductShipping: React.FC<ProductShippingProps> = props => {
error={!!formErrors.weight}
helperText={getProductErrorMessage(formErrors.weight, intl)}
name="weight"
type="number"
value={data.weight}
onChange={handleChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{weightUnit || ""}
</InputAdornment>
<InputAdornment position="end">{weightUnit || ""}</InputAdornment>
),
inputProps: {
min: 0,
},
}}
/>
</Grid>
</CardContent>
</Card>
</DashboardCard.Content>
</DashboardCard>
);
};
ProductShipping.displayName = "ProductShipping";
export default ProductShipping;

View file

@ -0,0 +1 @@
export * from "./ProductShipping";

View file

@ -2,11 +2,8 @@ import {
ChannelData,
ChannelPriceAndPreorderArgs,
} from "@dashboard/channels/utils";
import CardTitle from "@dashboard/components/CardTitle";
import ControlledCheckbox from "@dashboard/components/ControlledCheckbox";
import { DashboardCard } from "@dashboard/components/Card";
import { DateTimeTimezoneField } from "@dashboard/components/DateTimeTimezoneField";
import FormSpacer from "@dashboard/components/FormSpacer";
import Hr from "@dashboard/components/Hr";
import Link from "@dashboard/components/Link";
import PreviewPill from "@dashboard/components/PreviewPill";
import TableRowLink from "@dashboard/components/TableRowLink";
@ -17,22 +14,20 @@ import { sectionNames } from "@dashboard/intl";
import { renderCollection } from "@dashboard/misc";
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
import createNonNegativeValueChangeHandler from "@dashboard/utils/handlers/nonNegativeValueChangeHandler";
import { Table, TableBody, TableCell, TableHead } from "@material-ui/core";
import {
Card,
CardContent,
ClickAwayListener,
Grow,
MenuItem,
Paper,
Popper,
Table,
TableBody,
TableCell,
TableHead,
TextField,
Typography,
} from "@material-ui/core";
import { Button, DeleteIcon, IconButton, PlusIcon } from "@saleor/macaw-ui";
Box,
Button,
Checkbox,
Dropdown,
Input,
List,
PlusIcon,
sprinkles,
Text,
TrashBinIcon,
vars,
} from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -40,7 +35,6 @@ import { ProductCreateData } from "../ProductCreatePage";
import { ProductVariantCreateData } from "../ProductVariantCreatePage/form";
import { ProductVariantUpdateData } from "../ProductVariantPage/form";
import { messages } from "./messages";
import { useStyles } from "./styles";
export interface ProductStockFormsetData {
quantityAllocated: number;
@ -84,7 +78,7 @@ export interface ProductStocksProps {
onWarehouseConfigure: () => void;
}
const ProductStocks: React.FC<ProductStocksProps> = ({
export const ProductStocks: React.FC<ProductStocksProps> = ({
data,
disabled,
hasVariants,
@ -102,11 +96,8 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
onWarehouseStockDelete,
onWarehouseConfigure,
}) => {
const classes = useStyles();
const intl = useIntl();
const anchor = React.useRef<HTMLDivElement>();
const [lastStockRowFocus, setLastStockRowFocus] = React.useState(false);
const [isExpanded, setExpansionState] = React.useState(false);
const unitsLeft = parseInt(data.globalThreshold, 10) - data.globalSoldUnits;
const warehousesToAssign =
@ -141,85 +132,83 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
};
return (
<Card>
<CardTitle title={intl.formatMessage(messages.title)} />
<CardContent>
<div className={classes.skuInputContainer}>
<TextField
<DashboardCard>
<DashboardCard.Title>
{intl.formatMessage(messages.title)}
</DashboardCard.Title>
<DashboardCard.Content>
<Box __width="50%">
<Input
disabled={disabled}
error={!!formErrors.sku}
fullWidth
helperText={getProductErrorMessage(formErrors.sku, intl)}
label={intl.formatMessage(messages.sku)}
name="sku"
onChange={handleChange}
value={data.sku}
data-test-id="sku"
size="small"
helperText={getProductErrorMessage(formErrors.sku, intl)}
/>
</div>
<ControlledCheckbox
</Box>
<Box paddingY={5} display="grid" gap={5}>
<Checkbox
checked={data.isPreorder}
name="isPreorder"
onChange={
onEndPreorderTrigger && data.isPreorder
? onEndPreorderTrigger
: onFormDataChange
onCheckedChange={value => {
if (onEndPreorderTrigger && data.isPreorder) {
onEndPreorderTrigger();
} else {
onFormDataChange({ target: { name: "isPreorder", value } });
}
}}
disabled={disabled}
label={
<>
>
<Box display="flex" gap={3} paddingY={4}>
<Text>
<FormattedMessage {...messages.variantInPreorder} />
<PreviewPill className={classes.preview} />
</>
}
/>
</Text>
<PreviewPill />
</Box>
</Checkbox>
{!data.isPreorder && (
<>
<FormSpacer />
<ControlledCheckbox
<Checkbox
checked={data.trackInventory}
name="trackInventory"
onChange={onFormDataChange}
disabled={disabled}
label={
<>
<FormattedMessage {...messages.trackInventory} />
<Typography variant="caption">
<FormattedMessage {...messages.trackInventoryDescription} />
</Typography>
</>
onCheckedChange={value =>
onFormDataChange({ target: { name: "trackInventory", value } })
}
/>
</>
)}
</CardContent>
<Hr />
{!data.isPreorder && (
<CardContent className={classes.quantityContainer}>
<Typography>
<div className={classes.quantityHeader}>
<span>
<FormattedMessage {...messages.quantity} />
</span>
</div>
</Typography>
{!productVariantChannelListings?.length && (
<>
<FormSpacer />
<Typography variant="caption">
<FormattedMessage {...messages.noChannelWarehousesAllocation} />
</Typography>
</>
>
<Box display="flex" flexDirection="column">
<Text>
<FormattedMessage {...messages.trackInventory} />
</Text>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage {...messages.trackInventoryDescription} />
</Text>
</Box>
</Checkbox>
)}
{!data.isPreorder && (
<Box display="grid" gap={5}>
<Box display="flex" flexDirection="column">
<Text>
<FormattedMessage {...messages.quantity} />
</Text>
{!productVariantChannelListings?.length && (
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage
{...messages.noChannelWarehousesAllocation}
/>
</Text>
)}
</Box>
{!warehouses?.length && (
<Typography
color="textSecondary"
className={classes.noWarehouseInfo}
>
<Text color="textNeutralSubdued">
{hasVariants ? (
<>
<FormattedMessage
{...messages.configureWarehouseForVariant}
values={{
@ -228,9 +217,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
),
}}
/>
</>
) : (
<>
<FormattedMessage
{...messages.configureWarehouseForProduct}
values={{
@ -239,33 +226,35 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
),
}}
/>
</>
)}
</Typography>
</Text>
)}
</CardContent>
</Box>
)}
</Box>
</DashboardCard.Content>
{productVariantChannelListings?.length > 0 &&
warehouses?.length > 0 &&
!data.isPreorder && (
<Table>
<colgroup>
<col className={classes.colName} />
<col className={classes.colQuantity} />
<col className={classes.colQuantity} />
</colgroup>
<TableHead>
<TableRowLink>
<TableCell className={classes.colName}>
<TableCell style={{ paddingLeft: vars.space[9] }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage {...messages.warehouseName} />
</Text>
</TableCell>
<TableCell className={classes.colQuantity}>
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage {...messages.allocated} />
</Text>
</TableCell>
<TableCell className={classes.colQuantity}>
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage {...messages.quantity} />
</Text>
</TableCell>
<TableCell className={classes.colAction} />
<TableCell />
</TableRowLink>
</TableHead>
<TableBody>
@ -277,108 +266,98 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
return (
<TableRowLink key={stock.id}>
<TableCell className={classes.colName}>
{stock.label}
<TableCell style={{ paddingLeft: vars.space[9] }}>
<Text>{stock.label}</Text>
</TableCell>
<TableCell className={classes.colQuantity}>
{stock.data?.quantityAllocated || 0}
<TableCell>
<Text>{stock.data?.quantityAllocated || 0}</Text>
</TableCell>
<TableCell className={classes.colQuantity}>
<TextField
<TableCell>
<Input
data-test-id="stock-input"
disabled={disabled}
fullWidth
inputProps={{
className: classes.input,
min: 0,
type: "number",
}}
onChange={handleQuantityChange}
value={stock.value}
inputRef={input =>
size="small"
type="number"
min={0}
ref={input =>
stocks.length === index + 1 &&
handleStockInputFocus(input)
}
/>
</TableCell>
<TableCell className={classes.colAction}>
<IconButton
<TableCell>
<Button
type="button"
variant="secondary"
color="primary"
icon={<TrashBinIcon />}
onClick={() => onWarehouseStockDelete(stock.id)}
>
<DeleteIcon />
</IconButton>
/>
</TableCell>
</TableRowLink>
);
})}
{warehousesToAssign.length > 0 && (
<ClickAwayListener onClickAway={() => setExpansionState(false)}>
<TableRowLink
className={classes.addRow}
onClick={() => setExpansionState(!isExpanded)}
<Dropdown>
<Dropdown.Trigger>
<TableRowLink className={sprinkles({ cursor: "pointer" })}>
<TableCell
colSpan={3}
style={{ paddingLeft: vars.space[9] }}
>
<TableCell colSpan={3} className={classes.actionableText}>
<Typography variant="body2">
<Text>
<FormattedMessage {...messages.assignWarehouse} />
</Typography>
</Text>
</TableCell>
<TableCell className={classes.colAction}>
<div ref={anchor}>
<IconButton
data-test-id="add-warehouse"
color="primary"
<TableCell style={{ paddingRight: vars.space[9] }}>
<Button
type="button"
icon={<PlusIcon />}
variant="secondary"
className={classes.actionableText}
/>
</TableCell>
</TableRowLink>
</Dropdown.Trigger>
<Dropdown.Content align="end">
<Box>
<List
padding={5}
borderRadius={4}
boxShadow="overlay"
backgroundColor="surfaceNeutralPlain"
>
<PlusIcon />
</IconButton>
<Popper
className={classes.popper}
open={isExpanded}
anchorEl={anchor.current}
transition
placement="top-end"
>
{({ TransitionProps }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: "right top",
}}
>
<Paper className={classes.paper} elevation={8}>
{warehousesToAssign.map(warehouse => (
<MenuItem
className={classes.menuItem}
<Dropdown.Item key={warehouse.id}>
<List.Item
paddingX={4}
paddingY={5}
borderRadius={4}
onClick={() =>
handleWarehouseStockAdd(warehouse.id)
}
>
{warehouse.name}
</MenuItem>
<Text>{warehouse.name}</Text>
</List.Item>
</Dropdown.Item>
))}
</Paper>
</Grow>
)}
</Popper>
</div>
</TableCell>
</TableRowLink>
</ClickAwayListener>
</List>
</Box>
</Dropdown.Content>
</Dropdown>
)}
</TableBody>
</Table>
)}
{data.isPreorder && (
<CardContent>
<Typography variant="caption" className={classes.caption}>
<DashboardCard.Content>
<Box display="grid" gap={5}>
<Text variant="caption">
<FormattedMessage {...messages.preorderEndDateSetup} />
</Typography>
</Text>
{data.hasPreorderEndDate && (
<div className={classes.dateTimeInputs}>
<Box>
<DateTimeTimezoneField
name={"preorderEndDateTime"}
disabled={disabled}
@ -395,12 +374,15 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
})
}
/>
</div>
</Box>
)}
{/* @ts-ignore */}
<Box __alignSelf="end">
<Button
name="hasPreorderEndDate"
variant="tertiary"
variant="secondary"
disabled={disabled}
type="button"
onClick={() =>
onFormDataChange({
target: {
@ -414,58 +396,62 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
? intl.formatMessage(messages.endDateCancel)
: intl.formatMessage(messages.endDateSetup)}
</Button>
<Typography variant="caption" className={classes.preorderLimitInfo}>
</Box>
</Box>
<Box display="grid" gap={3} paddingTop={5}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage {...messages.preorderProductsAvailability} />
</Typography>
<div className={classes.thresholdRow}>
<TextField
inputProps={{
min: 0,
}}
</Text>
<Box display="grid" gap={4}>
<Box __width="50%">
<Input
min={0}
type="text"
disabled={disabled}
fullWidth
helperText={intl.formatMessage(
messages.preorderTresholdDescription,
)}
label={intl.formatMessage(messages.preorderTresholdLabel)}
name="globalThreshold"
onChange={onThresholdChange}
value={data.globalThreshold ?? ""}
className={classes.thresholdInput}
size="small"
helperText={intl.formatMessage(
messages.preorderTresholdDescription,
)}
/>
</Box>
{productVariantChannelListings?.length > 0 && (
<Typography
variant="caption"
className={classes.preorderItemsLeftCount}
>
<Text variant="caption">
{data.globalThreshold
? intl.formatMessage(messages.preorderTresholdUnitsLeft, {
unitsLeft,
})
: intl.formatMessage(messages.preorderTresholdUnlimited)}
</Typography>
</Text>
)}
</div>
</CardContent>
</Box>
</Box>
</DashboardCard.Content>
)}
{productVariantChannelListings?.length > 0 && data.isPreorder && (
<Table>
<colgroup>
<col className={classes.colName} />
<col className={classes.colSoldUnits} />
<col className={classes.colThreshold} />
</colgroup>
<TableHead>
<TableRowLink>
<TableCell className={classes.colName}>
<TableCell style={{ paddingLeft: vars.space[9] }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage {...sectionNames.channels} />
</Text>
</TableCell>
<TableCell className={classes.colSoldUnits}>
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage {...messages.soldUnits} />
</Text>
</TableCell>
<TableCell className={classes.colThreshold}>
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage {...messages.channelTreshold} />
</Text>
</TableCell>
</TableRowLink>
</TableHead>
@ -477,25 +463,18 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
return (
<TableRowLink key={listing.id}>
<TableCell className={classes.colName}>
{listing.name}
<TableCell style={{ paddingLeft: vars.space[9] }}>
<Text>{listing.name}</Text>
</TableCell>
<TableCell className={classes.colQuantity}>
{listing?.unitsSold || 0}
<TableCell>
<Text>{listing?.unitsSold || 0}</Text>
</TableCell>
<TableCell className={classes.colQuantity}>
<TextField
<TableCell>
<Input
min={0}
type="number"
name="channel-threshold"
disabled={disabled}
fullWidth
inputProps={{
className: classes.input,
min: 0,
type: "number",
}}
placeholder={intl.formatMessage(
messages.preorderTresholdUnlimited,
)}
onChange={e => {
onVariantChannelListingChange(listing.id, {
costPrice: listing.costPrice,
@ -507,6 +486,10 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
});
}}
value={listing?.preorderThreshold ?? ""}
size="small"
placeholder={intl.formatMessage(
messages.preorderTresholdUnlimited,
)}
/>
</TableCell>
</TableRowLink>
@ -515,9 +498,6 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
</TableBody>
</Table>
)}
</Card>
</DashboardCard>
);
};
ProductStocks.displayName = "ProductStocks";
export default ProductStocks;

View file

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

View file

@ -1,107 +0,0 @@
import { ICONBUTTON_SIZE, makeStyles } from "@saleor/macaw-ui";
import { vars } from "@saleor/macaw-ui/next";
export const useStyles = makeStyles(
theme => ({
colAction: {
padding: 0,
width: `calc(${ICONBUTTON_SIZE}px + ${theme.spacing(1)})`,
},
colName: {},
colQuantity: {
textAlign: "right",
width: 150,
},
colSoldUnits: {
textAlign: "right",
width: 150,
},
colThreshold: {
textAlign: "right",
width: 180,
},
editWarehouses: {
marginRight: theme.spacing(-1),
},
input: {
padding: theme.spacing(1.5),
textAlign: "right",
},
menuItem: {
"&:not(:last-of-type)": {
marginBottom: theme.spacing(2),
},
},
noWarehouseInfo: {
marginTop: theme.spacing(),
},
paper: {
padding: theme.spacing(2),
maxHeight: 400,
overflow: "scroll",
border: `1px solid ${vars.colors.border.neutralDefault}`,
borderRadius: vars.borderRadius[3],
},
popper: {
marginTop: theme.spacing(1),
zIndex: 2,
},
quantityContainer: {
paddingTop: theme.spacing(),
},
quantityHeader: {
alignItems: "center",
display: "flex",
justifyContent: "space-between",
},
skuInputContainer: {
display: "grid",
gridColumnGap: theme.spacing(3),
gridTemplateColumns: "repeat(2, 1fr)",
},
dateTimeInputs: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
preorderInfo: {
marginBottom: theme.spacing(2),
marginTop: theme.spacing(2),
display: "block",
},
caption: {
fontSize: 14,
},
thresholdRow: {
display: "grid",
gridColumnGap: theme.spacing(3),
gridTemplateColumns: "3fr 1fr",
marginTop: theme.spacing(1),
},
thresholdInput: {
maxWidth: 400,
},
addRow: {
"&:hover": {
cursor: "pointer",
"& $actionableText": {
color: theme.palette.primary.main,
},
},
},
actionableText: {},
preorderItemsLeftCount: {
fontSize: 14,
paddingTop: theme.spacing(2),
textAlign: "center",
},
preorderLimitInfo: {
marginTop: theme.spacing(3),
},
preview: {
marginLeft: theme.spacing(1),
},
}),
{
name: "ProductStocks",
},
);

View file

@ -56,7 +56,6 @@ const props: ProductUpdatePageProps = {
onSubmit: () => undefined,
onVariantShow: () => undefined,
refetch: () => undefined,
placeholderImage,
product,
referencePages: [],
referenceProducts: [],

View file

@ -66,7 +66,6 @@ const props: ProductUpdatePageProps = {
onMediaUrlUpload: () => undefined,
onSubmit,
onVariantShow: () => undefined,
placeholderImage,
product,
referencePages: [],
referenceProducts: [],

View file

@ -10,15 +10,14 @@ import {
import { ChannelData } from "@dashboard/channels/utils";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
import Attributes, { AttributeInput } from "@dashboard/components/Attributes";
import CardMenu from "@dashboard/components/CardMenu";
import { AttributeInput, Attributes } from "@dashboard/components/Attributes";
import CardSpacer from "@dashboard/components/CardSpacer";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata/Metadata";
import { Metadata } from "@dashboard/components/Metadata/Metadata";
import Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm";
import { SeoForm } from "@dashboard/components/SeoForm";
import { Choice } from "@dashboard/components/SingleSelectField";
import {
ChannelFragment,
@ -52,9 +51,9 @@ import React from "react";
import { useIntl } from "react-intl";
import { getChoices } from "../../utils/data";
import ProductDetailsForm from "../ProductDetailsForm";
import { ProductDetailsForm } from "../ProductDetailsForm";
import ProductMedia from "../ProductMedia";
import ProductOrganization from "../ProductOrganization";
import { ProductOrganization } from "../ProductOrganization";
import ProductTaxes from "../ProductTaxes";
import ProductVariants from "../ProductVariants";
import ProductUpdateForm from "./form";
@ -72,7 +71,6 @@ export interface ProductUpdatePageProps {
channelsErrors: ProductChannelListingErrorFragment[];
variantListErrors: ProductVariantListError[];
errors: UseProductUpdateHandlerError[];
placeholderImage: string;
collections: RelayToFlat<SearchCollectionsQuery["search"]>;
categories: RelayToFlat<SearchCategoriesQuery["search"]>;
attributeValues: RelayToFlat<
@ -139,7 +137,6 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
media,
header,
limits,
placeholderImage,
product,
saveButtonBarState,
variants,
@ -326,8 +323,8 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
return (
<DetailPageLayout>
<TopNav href={productListUrl()} title={header}>
<CardMenu
menuItems={[
<TopNav.Menu
items={[
...extensionMenuItems,
{
label: intl.formatMessage(messages.openGraphiQL),
@ -335,7 +332,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
testId: "graphiql-redirect",
},
]}
data-test-id="menu"
dataTestId="menu"
/>
</TopNav>
@ -349,7 +346,6 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
<CardSpacer />
<ProductMedia
media={media}
placeholderImage={placeholderImage}
onImageDelete={onImageDelete}
onImageReorder={onImageReorder}
onImageUpload={onImageUpload}

View file

@ -4,14 +4,15 @@ import {
} from "@dashboard/attributes/utils/data";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
import Attributes, {
import {
AttributeInput,
Attributes,
VariantAttributeScope,
} from "@dashboard/components/Attributes";
import CardSpacer from "@dashboard/components/CardSpacer";
import Grid from "@dashboard/components/Grid";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata";
import { Metadata } from "@dashboard/components/Metadata";
import Savebar from "@dashboard/components/Savebar";
import {
ProductErrorWithAttributesFragment,
@ -30,14 +31,14 @@ import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import ProductShipping from "../ProductShipping/ProductShipping";
import ProductStocks from "../ProductStocks";
import { ProductShipping } from "../ProductShipping";
import { ProductStocks } from "../ProductStocks";
import { useManageChannels } from "../ProductVariantChannels/useManageChannels";
import { VariantChannelsDialog } from "../ProductVariantChannels/VariantChannelsDialog";
import ProductVariantCheckoutSettings from "../ProductVariantCheckoutSettings/ProductVariantCheckoutSettings";
import ProductVariantName from "../ProductVariantName";
import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice";
import { ProductVariantPrice } from "../ProductVariantPrice";
import ProductVariantCreateForm, {
ProductVariantCreateData,
ProductVariantCreateHandlers,

View file

@ -5,15 +5,16 @@ import {
import { ChannelPriceData } from "@dashboard/channels/utils";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
import Attributes, {
import {
AttributeInput,
Attributes,
VariantAttributeScope,
} from "@dashboard/components/Attributes";
import CardSpacer from "@dashboard/components/CardSpacer";
import Grid from "@dashboard/components/Grid";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import { MetadataFormData } from "@dashboard/components/Metadata";
import Metadata from "@dashboard/components/Metadata/Metadata";
import { Metadata } from "@dashboard/components/Metadata/Metadata";
import Savebar from "@dashboard/components/Savebar";
import {
ProductChannelListingErrorFragment,
@ -33,8 +34,8 @@ import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
import React from "react";
import { defineMessages, useIntl } from "react-intl";
import ProductShipping from "../ProductShipping/ProductShipping";
import ProductStocks, { ProductStockInput } from "../ProductStocks";
import { ProductShipping } from "../ProductShipping";
import { ProductStockInput, ProductStocks } from "../ProductStocks";
import { useManageChannels } from "../ProductVariantChannels/useManageChannels";
import { VariantChannelsDialog } from "../ProductVariantChannels/VariantChannelsDialog";
import ProductVariantCheckoutSettings from "../ProductVariantCheckoutSettings/ProductVariantCheckoutSettings";
@ -43,7 +44,7 @@ import ProductVariantMediaSelectDialog from "../ProductVariantImageSelectDialog"
import ProductVariantMedia from "../ProductVariantMedia";
import ProductVariantName from "../ProductVariantName";
import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice";
import { ProductVariantPrice } from "../ProductVariantPrice";
import ProductVariantSetDefault from "../ProductVariantSetDefault";
import ProductVariantUpdateForm, {
ProductVariantUpdateData,

View file

@ -3,7 +3,7 @@ import {
ChannelPriceAndPreorderArgs,
ChannelPriceArgs,
} from "@dashboard/channels/utils";
import CardTitle from "@dashboard/components/CardTitle";
import { DashboardCard } from "@dashboard/components/Card";
import PriceField from "@dashboard/components/PriceField";
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
@ -15,47 +15,11 @@ import {
getFormChannelErrors,
} from "@dashboard/utils/errors";
import getProductErrorMessage from "@dashboard/utils/errors/product";
import {
Card,
CardContent,
TableBody,
TableCell,
TableHead,
Typography,
} from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import { TableBody, TableCell, TableHead } from "@material-ui/core";
import { Text, vars } from "@saleor/macaw-ui/next";
import React from "react";
import { FormattedMessage, MessageDescriptor, useIntl } from "react-intl";
const useStyles = makeStyles(
() => ({
colPrice: {
textAlign: "right",
verticalAlign: "middle",
width: 200,
},
colType: {
fontSize: 14,
textAlign: "right",
width: 200,
},
input: {
textAlign: "left",
},
pricingContent: {
"&:last-child": {
paddingBottom: 0,
},
paddingLeft: 0,
paddingRight: 0,
},
table: {
tableLayout: "fixed",
},
}),
{ name: "ProductVariantPrice" },
);
interface ProductVariantPriceProps {
ProductVariantChannelListings?: ChannelData[];
errors?: ProductChannelListingErrorFragment[];
@ -70,7 +34,9 @@ interface ProductVariantPriceProps {
const numberOfColumns = 2;
const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
export const ProductVariantPrice: React.FC<
ProductVariantPriceProps
> = props => {
const {
disabled = false,
errors = [],
@ -79,22 +45,21 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
onChange,
disabledMessage,
} = props;
const classes = useStyles(props);
const intl = useIntl();
const formErrors = getFormChannelErrors(["price", "costPrice"], errors);
if (disabled || !ProductVariantChannelListings.length) {
return (
<Card>
<CardTitle
title={intl.formatMessage({
<DashboardCard>
<DashboardCard.Title>
{intl.formatMessage({
id: "Xm9qOu",
defaultMessage: "Pricing",
description: "product pricing, section header",
})}
/>
<CardContent>
<Typography variant="caption">
</DashboardCard.Title>
<DashboardCard.Content>
<Text variant="caption">
{intl.formatMessage(
disabledMessage || {
id: "e48Igh",
@ -103,54 +68,60 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
description: "variant pricing section subtitle",
},
)}
</Typography>
</CardContent>
</Card>
</Text>
</DashboardCard.Content>
</DashboardCard>
);
}
return (
<Card>
<CardTitle
title={intl.formatMessage({
<DashboardCard>
<DashboardCard.Title>
{intl.formatMessage({
id: "Xm9qOu",
defaultMessage: "Pricing",
description: "product pricing, section header",
})}
/>
<CardContent>
<Typography variant="body2">
</DashboardCard.Title>
<DashboardCard.Content>
<Text variant="body">
{intl.formatMessage({
id: "VvA7ai",
defaultMessage:
"Channels that dont have assigned prices will use their parent channel to define the price. Price will be converted to channels currency",
description: "info text",
})}
</Typography>
</CardContent>
<ResponsiveTable className={classes.table}>
</Text>
</DashboardCard.Content>
<ResponsiveTable>
<TableHead>
<TableRowLink>
<TableCell>
<TableCell style={{ paddingLeft: vars.space[9] }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage
id="c8UT0c"
defaultMessage="Channel Name"
description="tabel column header"
/>
</Text>
</TableCell>
<TableCell className={classes.colType}>
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage
id="JFtFgc"
defaultMessage="Selling Price"
description="tabel column header"
/>
</Text>
</TableCell>
<TableCell className={classes.colType}>
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
<Text variant="caption" color="textNeutralSubdued">
<FormattedMessage
id="2zCmiR"
defaultMessage="Cost price"
description="tabel column header"
/>
</Text>
</TableCell>
</TableRowLink>
</TableHead>
@ -169,11 +140,12 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
return (
<TableRowLink key={listing?.id || `skeleton-${index}`}>
<TableCell>{listing?.name || <Skeleton />}</TableCell>
<TableCell className={classes.colPrice}>
<TableCell style={{ paddingLeft: vars.space[9] }}>
<Text>{listing?.name || <Skeleton />}</Text>
</TableCell>
<TableCell>
{listing ? (
<PriceField
className={classes.input}
error={!!priceError}
label={intl.formatMessage({
id: "b1zuN9",
@ -199,10 +171,9 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colPrice}>
<TableCell>
{listing ? (
<PriceField
className={classes.input}
error={!!costPriceError}
label={intl.formatMessage({
id: "KQSONM",
@ -236,18 +207,18 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
() => (
<TableRowLink>
<TableCell colSpan={numberOfColumns}>
<Text>
<FormattedMessage
id="/glQgs"
defaultMessage="No channels found"
/>
</Text>
</TableCell>
</TableRowLink>
),
)}
</TableBody>
</ResponsiveTable>
</Card>
</DashboardCard>
);
};
ProductVariantPrice.displayName = "ProductVariantPrice";
export default ProductVariantPrice;

View file

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

View file

@ -250,7 +250,7 @@ export const getPreorderEndHourFormData = (endDate?: string) =>
endDate ? moment(endDate).format("HH:mm") : "";
export const getSelectedMedia = <
T extends Pick<ProductMediaFragment, "id" | "sortOrder">
T extends Pick<ProductMediaFragment, "id" | "sortOrder">,
>(
media: T[] = [],
selectedMediaIds: string[],

View file

@ -1,4 +1,3 @@
import placeholderImg from "@assets/images/placeholder255x255.png";
import ActionDialog from "@dashboard/components/ActionDialog";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import { AttributeInput } from "@dashboard/components/Attributes";
@ -114,10 +113,8 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
},
});
const [
reorderProductImages,
reorderProductImagesOpts,
] = useProductMediaReorderMutation({});
const [reorderProductImages, reorderProductImagesOpts] =
useProductMediaReorderMutation({});
const [deleteProduct, deleteProductOpts] = useProductDeleteMutation({
onCompleted: () => {
@ -132,10 +129,8 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
},
});
const [
createProductImage,
createProductImageOpts,
] = useProductMediaCreateMutation({
const [createProductImage, createProductImageOpts] =
useProductMediaCreateMutation({
onCompleted: data => {
const imageError = data.productMediaCreate.errors.find(
error =>
@ -175,10 +170,8 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
},
});
const [
createProductMedia,
createProductMediaOpts,
] = useProductMediaCreateMutation({
const [createProductMedia, createProductMediaOpts] =
useProductMediaCreateMutation({
onCompleted: data => {
const errors = data.productMediaCreate.errors;
@ -282,7 +275,8 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
);
const fetchMoreAttributeValues = {
hasMore: !!searchAttributeValuesOpts.data?.attribute?.choices?.pageInfo
hasMore:
!!searchAttributeValuesOpts.data?.attribute?.choices?.pageInfo
?.hasNextPage,
loading: !!searchAttributeValuesOpts.loading,
onFetchMore: loadMoreAttributeValues,
@ -316,7 +310,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
saveButtonBarState={formTransitionState}
media={data?.product?.media}
header={product?.name}
placeholderImage={placeholderImg}
product={product}
warehouses={warehouses}
taxClasses={taxClasses ?? []}

View file

@ -3,7 +3,7 @@ import CardSpacer from "@dashboard/components/CardSpacer";
import CountryList from "@dashboard/components/CountryList";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
import Metadata from "@dashboard/components/Metadata/Metadata";
import { Metadata } from "@dashboard/components/Metadata/Metadata";
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
import Savebar from "@dashboard/components/Savebar";
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";

Some files were not shown because too many files have changed in this diff Show more