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 }) => { createCustomer(email, randomName, address, true).then(({ user }) => {
cy.visit(customerDetailsUrl(user.id)) cy.visit(customerDetailsUrl(user.id))
.get(CUSTOMER_DETAILS_SELECTORS.nameInput) .get(CUSTOMER_DETAILS_SELECTORS.nameInput)
.clearAndType(updatedName) .clear()
.type(updatedName)
.get(CUSTOMER_DETAILS_SELECTORS.lastNameInput) .get(CUSTOMER_DETAILS_SELECTORS.lastNameInput)
.clearAndType(updatedName) .clearAndType(updatedName)
.get(CUSTOMER_DETAILS_SELECTORS.noteInput) .get(CUSTOMER_DETAILS_SELECTORS.noteInput)
.clearAndType(updatedName) .clearAndType(updatedName)
.get(CUSTOMER_DETAILS_SELECTORS.emailInput) .get(CUSTOMER_DETAILS_SELECTORS.emailInput)
.clearAndType(`${updatedName}@example.com`) .clear()
.type(`${updatedName}@example.com`)
.addAliasToGraphRequest("UpdateCustomer") .addAliasToGraphRequest("UpdateCustomer")
.get(BUTTON_SELECTORS.confirm) .get(BUTTON_SELECTORS.confirm)
.click() .click()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ Cypress.Commands.add("clearAndType", { prevSubject: true }, (subject, text) => {
if (subject.find("[contenteditable]").length > 0) { if (subject.find("[contenteditable]").length > 0) {
cy.wrap(subject).find("[contenteditable]").clear().type(text); cy.wrap(subject).find("[contenteditable]").clear().type(text);
} else { } 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, entityType,
numericSystemType, numericSystemType,
swatchImage, swatchImage,
valueRequired = true valueRequired = true,
}) { }) {
fillUpAttributeCreateFields({ name, attributeType, valueRequired }); fillUpAttributeCreateFields({ name, attributeType, valueRequired });
if ( if (
@ -34,7 +34,7 @@ export function createAttributeWithInputType({
export function fillUpAttributeCreateFields({ export function fillUpAttributeCreateFields({
name, name,
attributeType, attributeType,
valueRequired valueRequired,
}) { }) {
fillUpAttributeNameAndCode(name); fillUpAttributeNameAndCode(name);
cy.get(ATTRIBUTES_DETAILS.inputTypeSelect) cy.get(ATTRIBUTES_DETAILS.inputTypeSelect)
@ -49,9 +49,11 @@ export function fillUpAttributeCreateFields({
export function fillUpAttributeNameAndCode(name, code = name) { export function fillUpAttributeNameAndCode(name, code = name) {
return cy return cy
.get(ATTRIBUTES_DETAILS.nameInput) .get(ATTRIBUTES_DETAILS.nameInput)
.clearAndType(name) .clear()
.type(name)
.get(ATTRIBUTES_DETAILS.codeInput) .get(ATTRIBUTES_DETAILS.codeInput)
.clearAndType(code); .clear()
.type(code);
} }
export function saveAttribute() { export function saveAttribute() {
@ -63,9 +65,7 @@ export function saveAttribute() {
} }
export function submitAttribute() { export function submitAttribute() {
cy.get(BUTTON_SELECTORS.confirm) cy.get(BUTTON_SELECTORS.confirm).click().confirmationMessageShouldDisappear();
.click()
.confirmationMessageShouldDisappear();
} }
export function addSingleValue(valueName) { export function addSingleValue(valueName) {

View file

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

View file

@ -35,7 +35,6 @@ export function createVariant({
cy.get(VARIANTS_SELECTORS.saveButton) cy.get(VARIANTS_SELECTORS.saveButton)
.click() .click()
.get(VARIANTS_SELECTORS.skuTextField) .get(VARIANTS_SELECTORS.skuTextField)
.find("input")
.should("be.enabled") .should("be.enabled")
.get(BUTTON_SELECTORS.back) .get(BUTTON_SELECTORS.back)
.click() .click()
@ -86,7 +85,7 @@ export function fillUpVariantDetails({
cy.get(VARIANTS_SELECTORS.variantNameInput).type(variantName); cy.get(VARIANTS_SELECTORS.variantNameInput).type(variantName);
} }
if (sku) { if (sku) {
cy.get(VARIANTS_SELECTORS.skuTextField).type(sku); cy.get(VARIANTS_SELECTORS.skuTextField).click({ force: true }).type(sku);
} }
if (warehouseName) { if (warehouseName) {
cy.get(VARIANTS_SELECTORS.addWarehouseButton).click(); cy.get(VARIANTS_SELECTORS.addWarehouseButton).click();
@ -114,7 +113,7 @@ export function fillUpVariantAttributeAndSku({ attributeName, sku }) {
.contains(attributeName) .contains(attributeName)
.click(); .click();
if (sku) { 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) .get(AVAILABLE_CHANNELS_FORM.assignedChannels)
.click() .click()
.get(manageSelector) .get(manageSelector)
.first()
.click() .click()
.waitForProgressBarToNotBeVisible() .waitForProgressBarToNotBeVisible()
.addAliasToGraphRequest("ProductChannelListingUpdate") .addAliasToGraphRequest("ProductChannelListingUpdate")

View file

@ -80,7 +80,9 @@ export function selectProductsOutOfStock() {
} }
export function selectFilterBy(filter) { 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) { export function selectFilterByAttribute(attributeSlug) {
@ -106,6 +108,8 @@ export function selectChannel(channelSlug) {
export function submitFilters() { export function submitFilters() {
cy.addAliasToGraphRequest("ProductList") cy.addAliasToGraphRequest("ProductList")
.get(BUTTON_SELECTORS.submit) .get(BUTTON_SELECTORS.submit)
.scrollIntoView()
.should("be.visible")
.click() .click()
.waitForRequestAndCheckIfNoErrors("@ProductList") .waitForRequestAndCheckIfNoErrors("@ProductList")
.get(PRODUCTS_LIST.emptyProductRow) .get(PRODUCTS_LIST.emptyProductRow)

View file

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

View file

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

View file

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

61
package-lock.json generated
View file

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

View file

@ -34,7 +34,7 @@
"@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/lab": "^4.0.0-alpha.61",
"@material-ui/styles": "^4.11.4", "@material-ui/styles": "^4.11.4",
"@reach/auto-id": "^0.16.0", "@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", "@saleor/sdk": "^0.5.0",
"@sentry/react": "^6.0.0", "@sentry/react": "^6.0.0",
"@types/faker": "^5.1.6", "@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 CardSpacer from "@dashboard/components/CardSpacer";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 { MetadataFormData } from "@dashboard/components/Metadata/types";
import Savebar from "@dashboard/components/Savebar"; import Savebar from "@dashboard/components/Savebar";
import { ListSettingsUpdate } from "@dashboard/components/TablePagination"; import { ListSettingsUpdate } from "@dashboard/components/TablePagination";

View file

@ -1,9 +1,9 @@
import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { CardSpacer } from "@dashboard/components/CardSpacer"; import { CardSpacer } from "@dashboard/components/CardSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm"; import { SeoForm } from "@dashboard/components/SeoForm";
import { ProductErrorFragment } from "@dashboard/graphql"; import { ProductErrorFragment } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui"; 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 { CardSpacer } from "@dashboard/components/CardSpacer";
import CardTitle from "@dashboard/components/CardTitle"; import CardTitle from "@dashboard/components/CardTitle";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 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 { Tab, TabContainer } from "@dashboard/components/Tab";
import { CategoryDetailsQuery, ProductErrorFragment } from "@dashboard/graphql"; import { CategoryDetailsQuery, ProductErrorFragment } from "@dashboard/graphql";
import { SubmitPromise } from "@dashboard/hooks/useForm"; 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 { CardSpacer } from "@dashboard/components/CardSpacer";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard"; import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm"; import { SeoForm } from "@dashboard/components/SeoForm";
import { import {
CollectionChannelListingErrorFragment, CollectionChannelListingErrorFragment,
CollectionErrorFragment, CollectionErrorFragment,

View file

@ -4,9 +4,9 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { CardSpacer } from "@dashboard/components/CardSpacer"; import { CardSpacer } from "@dashboard/components/CardSpacer";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard"; import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm"; import { SeoForm } from "@dashboard/components/SeoForm";
import { import {
CollectionChannelListingErrorFragment, CollectionChannelListingErrorFragment,
CollectionDetailsQuery, CollectionDetailsQuery,

View file

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

View file

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

View file

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

View file

@ -1,6 +1,4 @@
import { AttributeReference } from "@dashboard/attributes/utils/data"; import { AttributeReference } from "@dashboard/attributes/utils/data";
import CardTitle from "@dashboard/components/CardTitle";
import Hr from "@dashboard/components/Hr";
import { import {
AttributeEntityTypeEnum, AttributeEntityTypeEnum,
AttributeInputTypeEnum, AttributeInputTypeEnum,
@ -13,13 +11,12 @@ import {
import { FormsetAtomicData } from "@dashboard/hooks/useFormset"; import { FormsetAtomicData } from "@dashboard/hooks/useFormset";
import { FetchMoreProps } from "@dashboard/types"; import { FetchMoreProps } from "@dashboard/types";
import { RichTextGetters } from "@dashboard/utils/richText/useMultipleRichText"; import { RichTextGetters } from "@dashboard/utils/richText/useMultipleRichText";
import { Card, CardContent, Typography } from "@material-ui/core"; import { Accordion, Box, Divider, Text } from "@saleor/macaw-ui/next";
import { ChevronIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import clsx from "clsx";
import React from "react"; import React from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { defineMessages, FormattedMessage, useIntl } from "react-intl";
import AttributeRow from "./AttributeRow"; import { DashboardCard } from "../Card";
import { AttributeListItem } from "./AttributeListItem";
import { AttributeRowHandlers, VariantAttributeScope } from "./types"; import { AttributeRowHandlers, VariantAttributeScope } from "./types";
export interface AttributeInputData { export interface AttributeInputData {
@ -49,61 +46,6 @@ export interface AttributesProps extends AttributeRowHandlers {
richTextGetters: RichTextGetters<string>; 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({ const messages = defineMessages({
attributesNumber: { attributesNumber: {
id: "z0gGP+", id: "z0gGP+",
@ -117,7 +59,7 @@ const messages = defineMessages({
}, },
}); });
const Attributes: React.FC<AttributesProps> = ({ export const Attributes: React.FC<AttributesProps> = ({
attributes, attributes,
attributeValues, attributeValues,
errors, errors,
@ -127,66 +69,50 @@ const Attributes: React.FC<AttributesProps> = ({
...props ...props
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const classes = useStyles({});
const [expanded, setExpansionStatus] = React.useState(true);
const toggleExpansion = () => setExpansionStatus(!expanded);
return ( return (
<Card className={classes.card}> <DashboardCard>
<CardTitle title={title || intl.formatMessage(messages.header)} /> <DashboardCard.Title>
<CardContent className={classes.cardContent}> {title || intl.formatMessage(messages.header)}
<div className={classes.expansionBar}> </DashboardCard.Title>
<div className={classes.expansionBarLabelContainer}> <DashboardCard.Content>
<Typography className={classes.expansionBarLabel} variant="caption"> <Box display="flex" flexDirection="column" gap={5}>
<FormattedMessage <Accordion defaultValue="attributes-accordion">
{...messages.attributesNumber} <Accordion.Item value="attributes-accordion">
values={{ <Accordion.Item.Trigger buttonDataTestId="attributes-expand">
number: attributes.length, <Text variant="caption" color="textNeutralSubdued">
}} <FormattedMessage
/> {...messages.attributesNumber}
</Typography> values={{
</div> number: attributes.length,
<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 (
<React.Fragment key={attribute.id}>
{attributeIndex > 0 && <Hr />}
<AttributeRow
attribute={attribute}
attributeValues={attributeValues}
error={error}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={richTextGetters}
{...props}
/> />
</React.Fragment> </Text>
); </Accordion.Item.Trigger>
})} <Accordion.Item.Content>
</> {attributes.length > 0 && (
)} <ul>
</CardContent> <Divider />
</Card> {attributes.map((attribute, attributeIndex) => (
<React.Fragment key={attribute.id}>
{attributeIndex > 0 && <Divider />}
<AttributeListItem
attribute={attribute}
errors={errors}
attributeValues={attributeValues}
onAttributeSelectBlur={onAttributeSelectBlur}
richTextGetters={richTextGetters}
{...props}
/>
</React.Fragment>
))}
</ul>
)}
</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 { Box, InfoIcon, sprinkles, Text, Tooltip } from "@saleor/macaw-ui/next";
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 React from "react"; import React from "react";
import { useBasicAttributeStyles } from "./styles";
interface BasicAttributeRowProps { interface BasicAttributeRowProps {
label: string | React.ReactNode; label: string | React.ReactNode;
description?: 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, label,
description, description,
children, children,
flexValueContainer, id,
}) => { clickableLabel = false,
const classes = useBasicAttributeStyles(); }) => (
<Box
return ( as="li"
<Grid className={classes.attributeSection} variant="uniform"> justifyContent="space-between"
<div alignItems="center"
className={classes.attributeSectionLabel} paddingY={3}
data-test-id="attribute-label" paddingX={2}
> display="grid"
<Typography> gridTemplateColumns={2}
{label} gap={8}
{description && ( >
<Tooltip> <Box
<Tooltip.Trigger> data-test-id="attribute-label"
<HelpOutline className={classes.tooltipIcon} /> as="label"
</Tooltip.Trigger> htmlFor={id}
<Tooltip.Content side="bottom"> display="flex"
<Tooltip.Arrow /> gap={3}
{description} cursor={clickableLabel ? "pointer" : "auto"}
</Tooltip.Content> >
</Tooltip> <Text>{label}</Text>
)} {description && (
</Typography> <Tooltip>
</div> <Tooltip.Trigger>
<div <Box>
data-test-id="attribute-value" <InfoIcon
className={clsx(classes.value, { size="small"
[classes.flex]: flexValueContainer, className={sprinkles({
})} display: "block",
> })}
{children} />
</div> </Box>
</Grid> </Tooltip.Trigger>
); <Tooltip.Content side="top">
}; <Tooltip.Arrow />
{description}
BasicAttributeRow.displayName = "BasicAttributeRow"; </Tooltip.Content>
export default BasicAttributeRow; </Tooltip>
)}
</Box>
<Box data-test-id="attribute-value">{children}</Box>
</Box>
);

View file

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

View file

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

View file

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

View file

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

View file

@ -1,94 +1,28 @@
import { ChannelData } from "@dashboard/channels/utils"; import { ChannelData } from "@dashboard/channels/utils";
import Label from "@dashboard/orders/components/OrderHistory/Label"; import Label from "@dashboard/orders/components/OrderHistory/Label";
import { Typography } from "@material-ui/core"; import { Accordion, Text } from "@saleor/macaw-ui/next";
import { Accordion, AccordionSummary, makeStyles } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { Messages } from "../types"; 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 { export interface ChannelContentWrapperProps {
data: ChannelData; data: ChannelData;
children: React.ReactNode; children: React.ReactNode;
messages: Messages; messages: Messages;
} }
const ChannelContentWrapper: React.FC<ChannelContentWrapperProps> = ({ export const ChannelAvailabilityItemWrapper: React.FC<
data, ChannelContentWrapperProps
messages, > = ({ data: { name }, messages, children }) => (
children, <Accordion data-test-id="channel-availability-item">
}) => { <Accordion.Item value="channel-availability-item" gap={12}>
const expanderClasses = useExpanderStyles(); <Accordion.Item.Trigger buttonDataTestId="expand-icon">
const summaryClasses = useSummaryStyles(); <Text variant={"bodyEmp"}>{name}</Text>
const classes = useStyles(); <Label text={messages.availableDateText} />
</Accordion.Item.Trigger>
const { name } = data; <Accordion.Item.Content paddingLeft={6}>
{children}
return ( </Accordion.Item.Content>
<Accordion </Accordion.Item>
classes={expanderClasses} </Accordion>
data-test-id="channel-availability-item" );
>
<AccordionSummary className={summaryClasses.root}>
<div className={classes.container}>
<Typography>{name}</Typography>
<Label text={messages.availableDateText} />
</div>
</AccordionSummary>
{children}
</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 { Channel as ChannelList, ChannelData } from "@dashboard/channels/utils";
import Hr from "@dashboard/components/Hr";
import { PermissionEnum } from "@dashboard/graphql"; import { PermissionEnum } from "@dashboard/graphql";
import useDateLocalize from "@dashboard/hooks/useDateLocalize"; import useDateLocalize from "@dashboard/hooks/useDateLocalize";
import { RequireOnlyOne } from "@dashboard/misc"; 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 React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import ChannelAvailabilityItemContent from "./Channel/ChannelAvailabilityItemContent"; import {
import ChannelAvailabilityItemWrapper from "./Channel/ChannelAvailabilityItemWrapper"; ChannelAvailabilityItemContent,
import ChannelsAvailabilityCardWrapper, { ChannelAvailabilityItemWrapper,
} from "./Channel";
import {
ChannelsAvailabilityCardWrapper,
ChannelsAvailabilityWrapperProps, ChannelsAvailabilityWrapperProps,
} from "./ChannelsAvailabilityCardWrapper"; } from "./ChannelsAvailabilityCardWrapper";
import { useStyles } from "./styles";
import { ChannelOpts, ChannelsAvailabilityError, Messages } from "./types"; import { ChannelOpts, ChannelsAvailabilityError, Messages } from "./types";
import { getChannelsAvailabilityMessages } from "./utils"; import { getChannelsAvailabilityMessages } from "./utils";
@ -36,7 +37,9 @@ export type ChannelsAvailabilityCardProps = RequireOnlyOne<
"channels" | "channelsList" "channels" | "channelsList"
>; >;
export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = props => { export const ChannelsAvailability: React.FC<
ChannelsAvailabilityCardProps
> = props => {
const { const {
channelsList, channelsList,
errors = [], errors = [],
@ -49,7 +52,6 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = pro
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
const localizeDate = useDateLocalize(); const localizeDate = useDateLocalize();
const classes = useStyles({});
const channelsMessages = getChannelsAvailabilityMessages({ const channelsMessages = getChannelsAvailabilityMessages({
messages, messages,
@ -88,12 +90,10 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = pro
: channelsList : channelsList
? channelsList.map(data => ( ? channelsList.map(data => (
<React.Fragment key={data.id}> <React.Fragment key={data.id}>
<div className={classes.channelItem}> <Box>
<div className={classes.channelName}> <Text>{data.name}</Text>
<Typography>{data.name}</Typography> </Box>
</div> <Divider />
</div>
<Hr className={classes.hr} />
</React.Fragment> </React.Fragment>
)) ))
: null} : 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 RequirePermissions from "@dashboard/components/RequirePermissions";
import { PermissionEnum } from "@dashboard/graphql"; 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 React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useStyles } from "./styles"; import { DashboardCard } from "../Card";
export interface ChannelsAvailabilityWrapperProps { export interface ChannelsAvailabilityWrapperProps {
selectedChannelsCount: number; selectedChannelsCount: number;
@ -17,7 +14,9 @@ export interface ChannelsAvailabilityWrapperProps {
openModal: () => void; openModal: () => void;
} }
export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperProps> = props => { export const ChannelsAvailabilityCardWrapper: React.FC<
ChannelsAvailabilityWrapperProps
> = props => {
const { const {
selectedChannelsCount, selectedChannelsCount,
allChannelsCount, allChannelsCount,
@ -26,12 +25,11 @@ export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperPr
openModal, openModal,
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
const classes = useStyles({});
const channelsAvailabilityText = intl.formatMessage( const channelsAvailabilityText = intl.formatMessage(
{ {
id: "vY2lpx", id: "AD1PlC",
defaultMessage: 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", description: "channels availability text",
}, },
{ {
@ -41,43 +39,42 @@ export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperPr
); );
return ( return (
<> <DashboardCard>
<Card> <DashboardCard.Title>
<CardTitle <Box display="flex" justifyContent="space-between" alignItems="center">
title={intl.formatMessage({ <Box display={"flex"} flexDirection={"column"} gap={3}>
id: "5A6/2C", <div>
defaultMessage: "Availability", {intl.formatMessage({
description: "section header", id: "5A6/2C",
})} defaultMessage: "Availability",
toolbar={ description: "section header",
<RequirePermissions requiredPermissions={managePermissions}> })}
<Button </div>
onClick={openModal} {!!channelsAvailabilityText && (
data-test-id="channels-availability-manage-button" <Text variant={"caption"}>{channelsAvailabilityText}</Text>
> )}
{intl.formatMessage({ </Box>
id: "2i81/P", <RequirePermissions requiredPermissions={managePermissions}>
defaultMessage: "Manage", <Button
description: "section header button", onClick={openModal}
})} data-test-id="channels-availability-manage-button"
</Button> type="button"
</RequirePermissions> variant="secondary"
} >
/> {intl.formatMessage({
<CardContent className={classes.card}> id: "2i81/P",
{!!channelsAvailabilityText && ( defaultMessage: "Manage",
<> description: "section header button",
<Typography className={classes.channelInfo}> })}
{channelsAvailabilityText} </Button>
</Typography> </RequirePermissions>
<Hr className={classes.hr} /> </Box>
</> </DashboardCard.Title>
)} <DashboardCard.Content gap={3} display="flex" flexDirection="column">
<Box display="flex" flexDirection="column" gap={8}>
{children} {children}
</CardContent> </Box>
</Card> </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 { joinDateTime, splitDateTime } from "@dashboard/misc";
import { TextField } from "@material-ui/core"; import { TextField } from "@material-ui/core";
import { TextFieldProps } from "@material-ui/core/TextField"; import { TextFieldProps } from "@material-ui/core/TextField";
import { Box } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useStyles } from "./styles";
type DateTimeFieldProps = Omit<TextFieldProps, "label" | "error"> & { type DateTimeFieldProps = Omit<TextFieldProps, "label" | "error"> & {
onChange: (value: string) => void; onChange: (value: string) => void;
error: ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment; error: ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment;
@ -26,12 +25,11 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
value, value,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const classes = useStyles();
const parsedValue = value ? splitDateTime(value) : { date: "", time: "" }; const parsedValue = value ? splitDateTime(value) : { date: "", time: "" };
return ( return (
<> <Box display="flex" gap={2}>
<TextField <TextField
fullWidth fullWidth
disabled={disabled} disabled={disabled}
@ -47,11 +45,6 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
type="date" type="date"
value={parsedValue.date} value={parsedValue.date}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
InputProps={{
classes: {
root: classes.dateField,
},
}}
/> />
<TextField <TextField
fullWidth fullWidth
@ -68,12 +61,7 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
type="time" type="time"
value={parsedValue.time} value={parsedValue.time}
InputLabelProps={{ shrink: true }} 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 { FileFragment } from "@dashboard/graphql";
import { commonMessages } from "@dashboard/intl"; import { commonMessages } from "@dashboard/intl";
import { Typography } from "@material-ui/core"; import { Box, Button, Text, TrashBinIcon } from "@saleor/macaw-ui/next";
import { DeleteIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
@ -19,7 +17,6 @@ export interface FileUploadFieldProps {
React.InputHTMLAttributes<HTMLInputElement>, React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement HTMLInputElement
>; >;
className?: string;
disabled: boolean; disabled: boolean;
loading: boolean; loading: boolean;
file: FileChoiceType; file: FileChoiceType;
@ -29,44 +26,17 @@ export interface FileUploadFieldProps {
onFileDelete: () => void; 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 FileUploadField: React.FC<FileUploadFieldProps> = props => {
const { const {
loading, loading,
disabled, disabled,
file, file,
className,
error, error,
helperText, helperText,
onFileUpload, onFileUpload,
onFileDelete, onFileDelete,
inputProps, inputProps,
} = props; } = props;
const classes = useStyles({});
const intl = useIntl(); const intl = useIntl();
const fileInputAnchor = React.createRef<HTMLInputElement>(); const fileInputAnchor = React.createRef<HTMLInputElement>();
@ -85,52 +55,46 @@ const FileUploadField: React.FC<FileUploadFieldProps> = props => {
return ( return (
<> <>
<div className={className}> <Box display="flex" justifyContent="flex-end">
{file.label ? ( {file.label ? (
<div className={classes.uploadFileContent}> <Box display="flex" gap={5} alignItems="center">
<div className={classes.uploadFileName}> <Text variant="caption">
{loading ? ( {loading ? (
<Skeleton /> <Skeleton />
) : ( ) : (
<a <a href={file.file?.url} target="blank">
href={file.file?.url}
target="blank"
className={classes.fileUrl}
>
{file.label} {file.label}
</a> </a>
)} )}
</div> </Text>
<IconButton <Button
icon={<TrashBinIcon />}
variant="secondary" variant="secondary"
color="primary"
onClick={handleFileDelete} onClick={handleFileDelete}
disabled={disabled || loading} disabled={disabled || loading}
data-test-id="button-delete-file" data-test-id="button-delete-file"
> type="button"
<DeleteIcon /> />
</IconButton> </Box>
</div>
) : ( ) : (
<div> <Button
<Button onClick={clickFileInput}
onClick={clickFileInput} disabled={disabled || loading}
disabled={disabled || loading} variant="secondary"
variant="secondary" data-test-id="button-upload-file"
data-test-id="button-upload-file" type="button"
> >
{intl.formatMessage(commonMessages.chooseFile)} {intl.formatMessage(commonMessages.chooseFile)}
</Button> </Button>
</div>
)} )}
{error && ( {error && (
<Typography variant="caption" className={classes.errorText}> <Text variant="caption" color="textCriticalDefault">
{helperText} {helperText}
</Typography> </Text>
)} )}
</div> </Box>
<input <input
className={classes.fileField} style={{ display: "none" }}
id="fileUpload" id="fileUpload"
onChange={event => onFileUpload(event.target.files[0])} onChange={event => onFileUpload(event.target.files[0])}
type="file" type="file"

View file

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

View file

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

View file

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

View file

@ -1,5 +1,3 @@
export * from "./Metadata"; export * from "./Metadata";
export * from "./MetadataCard"; export * from "./MetadataCard";
export * from "./types"; 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 { ChangeEvent } from "@dashboard/hooks/useForm";
import { nameSeparator } from "./MetadataCard";
import { EventData, EventDataAction, EventDataField } from "./types"; 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 { export function parseEventData(event: ChangeEvent): EventData {
let action: EventDataAction; let action: EventDataAction;
let field: EventDataField = null; let field: EventDataField = null;

View file

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

View file

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

View file

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

View file

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

View file

@ -45,13 +45,15 @@ export interface SingleAutocompleteSelectFieldProps
nakedInput?: boolean; nakedInput?: boolean;
onBlur?: () => void; onBlur?: () => void;
popperPlacement?: PopperPlacementType; popperPlacement?: PopperPlacementType;
id?: string;
} }
const DebounceAutocomplete: React.ComponentType<DebounceProps< const DebounceAutocomplete: React.ComponentType<DebounceProps<string>> =
string Debounce;
>> = Debounce;
const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectFieldProps> = props => { const SingleAutocompleteSelectFieldComponent: React.FC<
SingleAutocompleteSelectFieldProps
> = props => {
const { const {
add, add,
allowCustomValues, allowCustomValues,
@ -78,6 +80,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
nakedInput = false, nakedInput = false,
onBlur, onBlur,
popperPlacement = "bottom-end", popperPlacement = "bottom-end",
id,
...rest ...rest
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
@ -243,6 +246,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
fullWidth={true} fullWidth={true}
onBlur={onBlur} onBlur={onBlur}
inputRef={input} inputRef={input}
id={id}
/> />
{isOpen && (!!inputValue || !!choices.length) && ( {isOpen && (!!inputValue || !!choices.length) && (
<Popper <Popper
@ -284,11 +288,9 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
); );
}; };
const SingleAutocompleteSelectField: React.FC<SingleAutocompleteSelectFieldProps> = ({ const SingleAutocompleteSelectField: React.FC<
choices, SingleAutocompleteSelectFieldProps
fetchChoices, > = ({ choices, fetchChoices, ...rest }) => {
...rest
}) => {
const [query, setQuery] = React.useState(""); const [query, setQuery] = React.useState("");
if (fetchChoices) { 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 { CardSpacer } from "@dashboard/components/CardSpacer";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 { MetadataFormData } from "@dashboard/components/Metadata/types";
import RequirePermissions from "@dashboard/components/RequirePermissions"; import RequirePermissions from "@dashboard/components/RequirePermissions";
import Savebar from "@dashboard/components/Savebar"; 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 ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import { createSaleChannelsChangeHandler } from "@dashboard/discounts/handlers"; import { createSaleChannelsChangeHandler } from "@dashboard/discounts/handlers";
import { saleListUrl } from "@dashboard/discounts/urls"; 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 ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import { Tab, TabContainer } from "@dashboard/components/Tab"; import { Tab, TabContainer } from "@dashboard/components/Tab";
import { import {

View file

@ -3,7 +3,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard"; import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import { import {
createChannelsChangeHandler, createChannelsChangeHandler,

View file

@ -5,7 +5,7 @@ import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailability
import CountryList from "@dashboard/components/CountryList"; import CountryList from "@dashboard/components/CountryList";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import { Tab, TabContainer } from "@dashboard/components/Tab"; import { Tab, TabContainer } from "@dashboard/components/Tab";
import { import {

View file

@ -1,6 +1,6 @@
import CardSpacer from "@dashboard/components/CardSpacer"; import CardSpacer from "@dashboard/components/CardSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import useNavigator from "@dashboard/hooks/useNavigator"; import useNavigator from "@dashboard/hooks/useNavigator";
import React from "react"; import React from "react";

View file

@ -9,7 +9,7 @@ import { CardSpacer } from "@dashboard/components/CardSpacer";
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks"; import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import { import {
OrderDetailsFragment, OrderDetailsFragment,

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import Form from "@dashboard/components/Form";
import Grid from "@dashboard/components/Grid"; import Grid from "@dashboard/components/Grid";
import Hr from "@dashboard/components/Hr"; import Hr from "@dashboard/components/Hr";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import { PageErrorFragment } from "@dashboard/graphql"; import { PageErrorFragment } from "@dashboard/graphql";
import useNavigator from "@dashboard/hooks/useNavigator"; 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 Grid from "@dashboard/components/Grid";
import Hr from "@dashboard/components/Hr"; import Hr from "@dashboard/components/Hr";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 { MetadataFormData } from "@dashboard/components/Metadata/types";
import Savebar from "@dashboard/components/Savebar"; import Savebar from "@dashboard/components/Savebar";
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";

View file

@ -4,12 +4,12 @@ import {
} from "@dashboard/attributes/utils/data"; } from "@dashboard/attributes/utils/data";
import { TopNav } from "@dashboard/components/AppLayout/TopNav"; import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog"; 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 CardSpacer from "@dashboard/components/CardSpacer";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import SeoForm from "@dashboard/components/SeoForm"; import { SeoForm } from "@dashboard/components/SeoForm";
import VisibilityCard from "@dashboard/components/VisibilityCard"; import VisibilityCard from "@dashboard/components/VisibilityCard";
import { import {
PageDetailsFragment, PageDetailsFragment,

View file

@ -2,7 +2,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import CardSpacer from "@dashboard/components/CardSpacer"; import CardSpacer from "@dashboard/components/CardSpacer";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 Savebar from "@dashboard/components/Savebar";
import { import {
ProductTypeKindEnum, ProductTypeKindEnum,

View file

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

View file

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

View file

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

View file

@ -1,2 +1 @@
export { default } from "./ProductDetailsForm";
export * 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 Form from "@dashboard/components/Form";
import FormSpacer from "@dashboard/components/FormSpacer"; import FormSpacer from "@dashboard/components/FormSpacer";
import { ProductFragment } from "@dashboard/graphql"; import { ProductFragment } from "@dashboard/graphql";
import { SubmitPromise } from "@dashboard/hooks/useForm"; import { SubmitPromise } from "@dashboard/hooks/useForm";
import { buttonMessages } from "@dashboard/intl";
import { import {
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
TextField,
Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import { Button, Input, Text } from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { defineMessages, FormattedMessage, useIntl } from "react-intl";
@ -52,33 +50,37 @@ const ProductExternalMediaDialog: React.FC<ProductExternalMediaDialogProps> = ({
return ( return (
<Dialog onClose={onClose} open={open}> <Dialog onClose={onClose} open={open}>
<DialogTitle disableTypography> <DialogTitle disableTypography>
{intl.formatMessage(messages.buttonMessage)} <Text variant="heading">
{intl.formatMessage(messages.buttonMessage)}
</Text>
</DialogTitle> </DialogTitle>
<Form initial={initialValues} onSubmit={handleOnSubmit}> <Form initial={initialValues} onSubmit={handleOnSubmit}>
{({ change, data, submit }) => ( {({ change, data, submit }) => (
<> <>
<DialogContent> <DialogContent>
<Typography> <Text variant="caption">
<FormattedMessage <FormattedMessage
id="zDvDnG" 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." 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" description="modal header"
/> />
</Typography> </Text>
<FormSpacer /> <FormSpacer />
<TextField <Input
label="URL" label="URL"
value={data.mediaUrl} value={data.mediaUrl}
name="mediaUrl" name="mediaUrl"
type="url" type="text"
onChange={change} onChange={change}
autoFocus autoFocus
fullWidth size="medium"
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<BackButton onClick={onClose} /> <Button variant="secondary" onClick={onClose}>
<FormattedMessage {...buttonMessages.back} />
</Button>
<Button onClick={submit}> <Button onClick={submit}>
{intl.formatMessage(messages.buttonMessage)} {intl.formatMessage(messages.buttonMessage)}
</Button> </Button>

View file

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

View file

@ -1,121 +1,23 @@
import { Button } from "@dashboard/components/Button"; import { DashboardCard } from "@dashboard/components/Card";
import CardTitle from "@dashboard/components/CardTitle";
import ImageUpload from "@dashboard/components/ImageUpload"; import ImageUpload from "@dashboard/components/ImageUpload";
import MediaTile from "@dashboard/components/MediaTile"; import MediaTile from "@dashboard/components/MediaTile";
import Skeleton from "@dashboard/components/Skeleton";
import { ProductMediaFragment, ProductMediaType } from "@dashboard/graphql"; import { ProductMediaFragment, ProductMediaType } from "@dashboard/graphql";
import { ProductMediaPopper } from "@dashboard/products/components/ProductMediaPopper/ProductMediaPopper";
import { ReorderAction } from "@dashboard/types"; import { ReorderAction } from "@dashboard/types";
import createMultiFileUploadHandler from "@dashboard/utils/handlers/multiFileUploadHandler"; import createMultiFileUploadHandler from "@dashboard/utils/handlers/multiFileUploadHandler";
import { Card, CardContent } from "@material-ui/core"; import {
import { makeStyles } from "@saleor/macaw-ui"; Box,
import { vars } from "@saleor/macaw-ui/next"; Button,
import clsx from "clsx"; Dropdown,
List,
sprinkles,
Text,
} from "@saleor/macaw-ui/next";
import React from "react"; import React from "react";
import { defineMessages, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { SortableContainer, SortableElement } from "react-sortable-hoc"; import { SortableContainer, SortableElement } from "react-sortable-hoc";
const messages = defineMessages({ import { messages } from "./messages";
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" },
);
interface SortableMediaProps { interface SortableMediaProps {
media: { media: {
@ -163,7 +65,6 @@ const MediaListContainer = SortableContainer<MediaListContainerProps>(
); );
interface ProductMediaProps { interface ProductMediaProps {
placeholderImage?: string;
media: ProductMediaFragment[]; media: ProductMediaFragment[];
loading?: boolean; loading?: boolean;
getImageEditUrl: (id: string) => string; getImageEditUrl: (id: string) => string;
@ -176,7 +77,6 @@ interface ProductMediaProps {
const ProductMedia: React.FC<ProductMediaProps> = props => { const ProductMedia: React.FC<ProductMediaProps> = props => {
const { const {
media, media,
placeholderImage,
getImageEditUrl, getImageEditUrl,
onImageDelete, onImageDelete,
onImageReorder, onImageReorder,
@ -184,14 +84,12 @@ const ProductMedia: React.FC<ProductMediaProps> = props => {
openMediaUrlModal, openMediaUrlModal,
} = props; } = props;
const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
const imagesUpload = React.useRef<HTMLInputElement>(null); const imagesUpload = React.useRef<HTMLInputElement>(null);
const anchor = React.useRef<HTMLButtonElement>(); const anchor = React.useRef<HTMLButtonElement>();
const [imagesToUpload, setImagesToUpload] = React.useState< const [imagesToUpload, setImagesToUpload] = React.useState<
ProductMediaFragment[] ProductMediaFragment[]
>([]); >([]);
const [popperOpenStatus, setPopperOpenStatus] = React.useState(false);
const handleImageUpload = createMultiFileUploadHandler(onImageUpload, { const handleImageUpload = createMultiFileUploadHandler(onImageUpload, {
onAfterUpload: () => onAfterUpload: () =>
@ -219,61 +117,92 @@ const ProductMedia: React.FC<ProductMediaProps> = props => {
}); });
return ( return (
<Card className={classes.card}> <DashboardCard>
<CardTitle <DashboardCard.Title>
title={intl.formatMessage(messages.media)} <Box display="flex" justifyContent="space-between" cursor="pointer">
toolbar={ <FormattedMessage {...messages.media} />
<> <Dropdown>
<Button <Dropdown.Trigger>
onClick={() => setPopperOpenStatus(true)} <Button
variant="tertiary" variant="secondary"
data-test-id="button-upload-image" type="button"
ref={anchor} data-test-id="button-upload-image"
> ref={anchor}
{intl.formatMessage(messages.upload)} >
</Button> {intl.formatMessage(messages.upload)}
</Button>
<ProductMediaPopper </Dropdown.Trigger>
anchorRef={anchor.current} <Dropdown.Content align="end">
imagesUploadRef={imagesUpload.current} <List
setPopperStatus={setPopperOpenStatus} padding={5}
popperStatus={popperOpenStatus} borderRadius={4}
openMediaUrlModal={openMediaUrlModal} boxShadow="overlay"
/> backgroundColor="surfaceNeutralPlain"
>
<input <Dropdown.Item>
className={classes.fileField} <List.Item
id="fileUpload" borderRadius={4}
onChange={event => handleImageUpload(event.target.files)} paddingX={4}
multiple paddingY={5}
type="file" onClick={() => imagesUpload.current.click()}
ref={imagesUpload} data-test-id="upload-images"
accept="image/*" >
/> <Text>{intl.formatMessage(messages.uploadImages)}</Text>
</> </List.Item>
} </Dropdown.Item>
/> <Dropdown.Item>
<div className={classes.imageGridContainer}> <List.Item
{media === undefined ? ( borderRadius={4}
<CardContent> paddingX={4}
<div className={classes.root}> paddingY={5}
<div className={classes.imageContainer}> onClick={openMediaUrlModal}
<img className={classes.image} src={placeholderImage} /> data-test-id="upload-media-url"
</div> >
</div> <Text>{intl.formatMessage(messages.uploadUrl)}</Text>
</CardContent> </List.Item>
) : media.length > 0 ? ( </Dropdown.Item>
<> </List>
<ImageUpload </Dropdown.Content>
className={classes.imageUpload} </Dropdown>
isActiveClassName={classes.imageUploadActive} </Box>
disableClick={true} </DashboardCard.Title>
hideUploadIcon={true} <DashboardCard.Content>
iconContainerActiveClassName={classes.imageUploadIconActive} <Box>
onImageUpload={handleImageUpload} <Box
> as="input"
{({ isDragActive }) => ( display="none"
<CardContent> id="fileUpload"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
handleImageUpload(event.target.files)
}
multiple
type="file"
ref={imagesUpload}
accept="image/*"
/>
</Box>
<Box position="relative">
{media === undefined ? (
<Box padding={8}>
<Skeleton />
</Box>
) : media.length > 0 ? (
<>
<ImageUpload
className={sprinkles({
height: "100%",
width: "100%",
position: "absolute",
top: 0,
left: 0,
})}
isActiveClassName={sprinkles({ zIndex: "1" })}
disableClick={true}
hideUploadIcon={true}
iconContainerActiveClassName={sprinkles({ display: "block" })}
onImageUpload={handleImageUpload}
>
{({ isDragActive }) => (
<MediaListContainer <MediaListContainer
distance={20} distance={20}
helperClass="dragged" helperClass="dragged"
@ -281,22 +210,24 @@ const ProductMedia: React.FC<ProductMediaProps> = props => {
media={media} media={media}
preview={imagesToUpload} preview={imagesToUpload}
onSortEnd={onImageReorder} onSortEnd={onImageReorder}
className={clsx({ className={sprinkles({
[classes.root]: true, display: "grid",
[classes.rootDragActive]: isDragActive, gap: 8,
gridTemplateColumns: { mobile: 2, tablet: 3, desktop: 4 },
opacity: isDragActive ? "0.2" : "1",
})} })}
onDelete={onImageDelete} onDelete={onImageDelete}
getEditHref={getImageEditUrl} getEditHref={getImageEditUrl}
/> />
</CardContent> )}
)} </ImageUpload>
</ImageUpload> </>
</> ) : (
) : ( <ImageUpload onImageUpload={handleImageUpload} />
<ImageUpload onImageUpload={handleImageUpload} /> )}
)} </Box>
</div> </DashboardCard.Content>
</Card> </DashboardCard>
); );
}; };
ProductMedia.displayName = "ProductMedia"; 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 { DashboardCard } from "@dashboard/components/Card";
import CardTitle from "@dashboard/components/CardTitle";
import { FormSpacer } from "@dashboard/components/FormSpacer";
import Hr from "@dashboard/components/Hr";
import Link from "@dashboard/components/Link"; import Link from "@dashboard/components/Link";
import MultiAutocompleteSelectField, { import MultiAutocompleteSelectField, {
MultiAutocompleteChoiceType, MultiAutocompleteChoiceType,
@ -16,12 +13,10 @@ import {
} from "@dashboard/graphql"; } from "@dashboard/graphql";
import { ChangeEvent } from "@dashboard/hooks/useForm"; import { ChangeEvent } from "@dashboard/hooks/useForm";
import { commonMessages } from "@dashboard/intl"; import { commonMessages } from "@dashboard/intl";
import { maybe } from "@dashboard/misc";
import { productTypeUrl } from "@dashboard/productTypes/urls"; import { productTypeUrl } from "@dashboard/productTypes/urls";
import { FetchMoreProps } from "@dashboard/types"; import { FetchMoreProps } from "@dashboard/types";
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors"; import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
import { Card, CardContent, Typography } from "@material-ui/core"; import { Box, Text } from "@saleor/macaw-ui/next";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -31,22 +26,6 @@ interface ProductType {
name: string; 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 { interface ProductOrganizationProps {
canChangeType: boolean; canChangeType: boolean;
categories?: SingleAutocompleteChoiceType[]; categories?: SingleAutocompleteChoiceType[];
@ -74,7 +53,9 @@ interface ProductOrganizationProps {
onProductTypeChange?: (event: ChangeEvent) => void; onProductTypeChange?: (event: ChangeEvent) => void;
} }
const ProductOrganization: React.FC<ProductOrganizationProps> = props => { export const ProductOrganization: React.FC<
ProductOrganizationProps
> = props => {
const { const {
canChangeType, canChangeType,
categories, categories,
@ -98,7 +79,6 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
onProductTypeChange, onProductTypeChange,
} = props; } = props;
const classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
const formErrors = getFormErrors( const formErrors = getFormErrors(
@ -111,15 +91,15 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
: null; : null;
return ( return (
<Card className={classes.card}> <DashboardCard>
<CardTitle <DashboardCard.Title>
title={intl.formatMessage({ {intl.formatMessage({
id: "JjeZEG", id: "JjeZEG",
defaultMessage: "Organize Product", defaultMessage: "Organize Product",
description: "section header", description: "section header",
})} })}
/> </DashboardCard.Title>
<CardContent> <DashboardCard.Content gap={8} display="flex" flexDirection="column">
{canChangeType ? ( {canChangeType ? (
<SingleAutocompleteSelectField <SingleAutocompleteSelectField
displayValue={productTypeInputDisplayValue} displayValue={productTypeInputDisplayValue}
@ -139,36 +119,33 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
{...fetchMoreProductTypes} {...fetchMoreProductTypes}
/> />
) : ( ) : (
<> <Box display="flex" flexDirection="column" gap={6}>
<Typography className={classes.label} variant="caption"> <Box display="flex" flexDirection="column">
<FormattedMessage id="anK7jD" defaultMessage="Product Type" /> <Text variant="bodyEmp">
</Typography> <FormattedMessage id="anK7jD" defaultMessage="Product Type" />
<Typography> </Text>
<Link <Text variant="caption">
href={productTypeUrl(productType?.id) ?? ""} <Link
disabled={!productType?.id} href={productTypeUrl(productType?.id) ?? ""}
> disabled={!productType?.id}
{productType?.name ?? "..."} >
</Link> {productType?.name ?? "..."}
</Typography> </Link>
<CardSpacer /> </Text>
<Typography className={classes.label} variant="caption"> </Box>
<FormattedMessage id="Be+J13" defaultMessage="Configurable" />
</Typography> <Box display="flex" flexDirection="column">
<Typography> <Text variant="bodyEmp">
{maybe( <FormattedMessage id="Be+J13" defaultMessage="Configurable" />
() => </Text>
productType.hasVariants <Text variant="caption">
? intl.formatMessage(commonMessages.yes) {productType?.hasVariants
: intl.formatMessage(commonMessages.no), ? intl.formatMessage(commonMessages.yes)
"...", : intl.formatMessage(commonMessages.no)}
)} </Text>
</Typography> </Box>
</> </Box>
)} )}
<FormSpacer />
<Hr />
<FormSpacer />
<SingleAutocompleteSelectField <SingleAutocompleteSelectField
displayValue={categoryInputDisplayValue} displayValue={categoryInputDisplayValue}
error={!!(formErrors.category || noCategoryError)} error={!!(formErrors.category || noCategoryError)}
@ -189,9 +166,6 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
data-test-id="category" data-test-id="category"
{...fetchMoreCategories} {...fetchMoreCategories}
/> />
<FormSpacer />
<Hr />
<FormSpacer />
<MultiAutocompleteSelectField <MultiAutocompleteSelectField
displayValues={collectionsInputDisplayValue} displayValues={collectionsInputDisplayValue}
error={!!formErrors.collections} error={!!formErrors.collections}
@ -217,9 +191,7 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
testId="collection" testId="collection"
{...fetchMoreCollections} {...fetchMoreCollections}
/> />
</CardContent> </DashboardCard.Content>
</Card> </DashboardCard>
); );
}; };
ProductOrganization.displayName = "ProductOrganization";
export default ProductOrganization;

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +1 @@
export * from "./ProductStocks"; 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, onSubmit: () => undefined,
onVariantShow: () => undefined, onVariantShow: () => undefined,
refetch: () => undefined, refetch: () => undefined,
placeholderImage,
product, product,
referencePages: [], referencePages: [],
referenceProducts: [], referenceProducts: [],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import placeholderImg from "@assets/images/placeholder255x255.png";
import ActionDialog from "@dashboard/components/ActionDialog"; import ActionDialog from "@dashboard/components/ActionDialog";
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
import { AttributeInput } from "@dashboard/components/Attributes"; import { AttributeInput } from "@dashboard/components/Attributes";
@ -114,10 +113,8 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
}, },
}); });
const [ const [reorderProductImages, reorderProductImagesOpts] =
reorderProductImages, useProductMediaReorderMutation({});
reorderProductImagesOpts,
] = useProductMediaReorderMutation({});
const [deleteProduct, deleteProductOpts] = useProductDeleteMutation({ const [deleteProduct, deleteProductOpts] = useProductDeleteMutation({
onCompleted: () => { onCompleted: () => {
@ -132,25 +129,23 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
}, },
}); });
const [ const [createProductImage, createProductImageOpts] =
createProductImage, useProductMediaCreateMutation({
createProductImageOpts, onCompleted: data => {
] = useProductMediaCreateMutation({ const imageError = data.productMediaCreate.errors.find(
onCompleted: data => { error =>
const imageError = data.productMediaCreate.errors.find( error.field ===
error => ("image" as keyof ProductMediaCreateMutationVariables),
error.field === );
("image" as keyof ProductMediaCreateMutationVariables), if (imageError) {
); notify({
if (imageError) { status: "error",
notify({ title: intl.formatMessage(errorMessages.imgageUploadErrorTitle),
status: "error", text: intl.formatMessage(errorMessages.imageUploadErrorText),
title: intl.formatMessage(errorMessages.imgageUploadErrorTitle), });
text: intl.formatMessage(errorMessages.imageUploadErrorText), }
}); },
} });
},
});
const [deleteProductImage] = useProductMediaDeleteMutation({ const [deleteProductImage] = useProductMediaDeleteMutation({
onCompleted: () => onCompleted: () =>
@ -175,28 +170,26 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
}, },
}); });
const [ const [createProductMedia, createProductMediaOpts] =
createProductMedia, useProductMediaCreateMutation({
createProductMediaOpts, onCompleted: data => {
] = useProductMediaCreateMutation({ const errors = data.productMediaCreate.errors;
onCompleted: data => {
const errors = data.productMediaCreate.errors;
if (errors.length) { if (errors.length) {
errors.map(error => errors.map(error =>
notify({
status: "error",
text: getProductErrorMessage(error, intl),
}),
);
} else {
notify({ notify({
status: "error", status: "success",
text: getProductErrorMessage(error, intl), text: intl.formatMessage(commonMessages.savedChanges),
}), });
); }
} else { },
notify({ });
status: "success",
text: intl.formatMessage(commonMessages.savedChanges),
});
}
},
});
const handleMediaUrlUpload = (mediaUrl: string) => { const handleMediaUrlUpload = (mediaUrl: string) => {
const variables = { const variables = {
@ -282,8 +275,9 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
); );
const fetchMoreAttributeValues = { const fetchMoreAttributeValues = {
hasMore: !!searchAttributeValuesOpts.data?.attribute?.choices?.pageInfo hasMore:
?.hasNextPage, !!searchAttributeValuesOpts.data?.attribute?.choices?.pageInfo
?.hasNextPage,
loading: !!searchAttributeValuesOpts.loading, loading: !!searchAttributeValuesOpts.loading,
onFetchMore: loadMoreAttributeValues, onFetchMore: loadMoreAttributeValues,
}; };
@ -316,7 +310,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
saveButtonBarState={formTransitionState} saveButtonBarState={formTransitionState}
media={data?.product?.media} media={data?.product?.media}
header={product?.name} header={product?.name}
placeholderImage={placeholderImg}
product={product} product={product}
warehouses={warehouses} warehouses={warehouses}
taxClasses={taxClasses ?? []} taxClasses={taxClasses ?? []}

View file

@ -3,7 +3,7 @@ import CardSpacer from "@dashboard/components/CardSpacer";
import CountryList from "@dashboard/components/CountryList"; import CountryList from "@dashboard/components/CountryList";
import Form from "@dashboard/components/Form"; import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts"; 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 { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
import Savebar from "@dashboard/components/Savebar"; import Savebar from "@dashboard/components/Savebar";
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField"; import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";

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