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:
parent
4aa648353e
commit
180e3c56e6
102 changed files with 1589 additions and 2215 deletions
|
@ -263,13 +263,15 @@ describe("Tests for customer", () => {
|
|||
createCustomer(email, randomName, address, true).then(({ user }) => {
|
||||
cy.visit(customerDetailsUrl(user.id))
|
||||
.get(CUSTOMER_DETAILS_SELECTORS.nameInput)
|
||||
.clearAndType(updatedName)
|
||||
.clear()
|
||||
.type(updatedName)
|
||||
.get(CUSTOMER_DETAILS_SELECTORS.lastNameInput)
|
||||
.clearAndType(updatedName)
|
||||
.get(CUSTOMER_DETAILS_SELECTORS.noteInput)
|
||||
.clearAndType(updatedName)
|
||||
.get(CUSTOMER_DETAILS_SELECTORS.emailInput)
|
||||
.clearAndType(`${updatedName}@example.com`)
|
||||
.clear()
|
||||
.type(`${updatedName}@example.com`)
|
||||
.addAliasToGraphRequest("UpdateCustomer")
|
||||
.get(BUTTON_SELECTORS.confirm)
|
||||
.click()
|
||||
|
|
|
@ -193,7 +193,9 @@ describe("As an admin I want to manage warehouses", () => {
|
|||
warehouse = warehouseResp;
|
||||
cy.visit(warehouseDetailsUrl(warehouse.id))
|
||||
.get(WAREHOUSES_DETAILS.nameInput)
|
||||
.clearAndType(updatedName)
|
||||
.clear()
|
||||
.type(updatedName)
|
||||
// .clearAndType(updatedName)
|
||||
.fillUpBasicAddress(secondUsAddress)
|
||||
.addAliasToGraphRequest("WarehouseUpdate")
|
||||
.get(BUTTON_SELECTORS.confirm)
|
||||
|
|
|
@ -162,7 +162,9 @@ describe("Tests for pages", () => {
|
|||
.then(({ page }) => {
|
||||
cy.visit(pageDetailsUrl(page.id))
|
||||
.get(PAGE_DETAILS_SELECTORS.nameInput)
|
||||
.clearAndType(updatedName)
|
||||
.clear()
|
||||
.type(updatedName)
|
||||
// .clearAndType(updatedName)
|
||||
.get(PAGE_DETAILS_SELECTORS.isNotPublishedCheckbox)
|
||||
.click()
|
||||
.addAliasToGraphRequest("PageUpdate")
|
||||
|
|
|
@ -192,7 +192,6 @@ describe("Updating products without sku", () => {
|
|||
.get(SHARED_ELEMENTS.skeleton)
|
||||
.should("not.exist")
|
||||
.get(VARIANTS_SELECTORS.skuTextField)
|
||||
.find("input")
|
||||
.clear()
|
||||
.addAliasToGraphRequest("VariantUpdate")
|
||||
.get(VARIANTS_SELECTORS.variantNameInput)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export const AVAILABLE_CHANNELS_FORM = {
|
||||
manageChannelsButton: "[data-test-id='channels-availability-manage-button']",
|
||||
assignedChannels: "[data-test-id='expand-icon']",
|
||||
publishedRadioButtons: "[name*='isPublished']",
|
||||
publishedRadioButtons: "[name*='isPublished'] > ",
|
||||
availableForPurchaseRadioButtons: "[name*='isAvailableForPurchase']",
|
||||
radioButtonsValueTrue: "[value='true']",
|
||||
radioButtonsValueFalse: "[value='false']",
|
||||
|
|
|
@ -6,6 +6,6 @@ export const PAGE_DETAILS_SELECTORS = {
|
|||
isNotPublishedCheckbox: '[name="isPublished"][value=false]',
|
||||
uploadFileButton: '[data-test-id="button-upload-file"]',
|
||||
richTextEditorAttributeValue: '[class*="ce-paragraph"]',
|
||||
booleanAttributeValueCheckbox: '[name*="attribute:"][type="checkbox"]',
|
||||
booleanAttributeValueCheckbox: '[role="checkbox"]',
|
||||
numericAttributeValueInput: '[name*="attribute:"]',
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ Cypress.Commands.add("clearAndType", { prevSubject: true }, (subject, text) => {
|
|||
if (subject.find("[contenteditable]").length > 0) {
|
||||
cy.wrap(subject).find("[contenteditable]").clear().type(text);
|
||||
} else {
|
||||
cy.wrap(subject).clear().type(text);
|
||||
cy.wrap(subject).clear({ force: true }).type(text);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ export function createAttributeWithInputType({
|
|||
entityType,
|
||||
numericSystemType,
|
||||
swatchImage,
|
||||
valueRequired = true
|
||||
valueRequired = true,
|
||||
}) {
|
||||
fillUpAttributeCreateFields({ name, attributeType, valueRequired });
|
||||
if (
|
||||
|
@ -34,7 +34,7 @@ export function createAttributeWithInputType({
|
|||
export function fillUpAttributeCreateFields({
|
||||
name,
|
||||
attributeType,
|
||||
valueRequired
|
||||
valueRequired,
|
||||
}) {
|
||||
fillUpAttributeNameAndCode(name);
|
||||
cy.get(ATTRIBUTES_DETAILS.inputTypeSelect)
|
||||
|
@ -49,9 +49,11 @@ export function fillUpAttributeCreateFields({
|
|||
export function fillUpAttributeNameAndCode(name, code = name) {
|
||||
return cy
|
||||
.get(ATTRIBUTES_DETAILS.nameInput)
|
||||
.clearAndType(name)
|
||||
.clear()
|
||||
.type(name)
|
||||
.get(ATTRIBUTES_DETAILS.codeInput)
|
||||
.clearAndType(code);
|
||||
.clear()
|
||||
.type(code);
|
||||
}
|
||||
|
||||
export function saveAttribute() {
|
||||
|
@ -63,9 +65,7 @@ export function saveAttribute() {
|
|||
}
|
||||
|
||||
export function submitAttribute() {
|
||||
cy.get(BUTTON_SELECTORS.confirm)
|
||||
.click()
|
||||
.confirmationMessageShouldDisappear();
|
||||
cy.get(BUTTON_SELECTORS.confirm).click().confirmationMessageShouldDisappear();
|
||||
}
|
||||
|
||||
export function addSingleValue(valueName) {
|
||||
|
|
|
@ -25,6 +25,7 @@ export function createCollection(collectionName, isPublished, channel) {
|
|||
.get(AVAILABLE_CHANNELS_FORM.availableChannel)
|
||||
.click()
|
||||
.get(`${AVAILABLE_CHANNELS_FORM.publishedRadioButtons}${publishedSelector}`)
|
||||
.first()
|
||||
.click();
|
||||
return saveCollection().its("response.body.data.collectionCreate.collection");
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ export function createVariant({
|
|||
cy.get(VARIANTS_SELECTORS.saveButton)
|
||||
.click()
|
||||
.get(VARIANTS_SELECTORS.skuTextField)
|
||||
.find("input")
|
||||
.should("be.enabled")
|
||||
.get(BUTTON_SELECTORS.back)
|
||||
.click()
|
||||
|
@ -86,7 +85,7 @@ export function fillUpVariantDetails({
|
|||
cy.get(VARIANTS_SELECTORS.variantNameInput).type(variantName);
|
||||
}
|
||||
if (sku) {
|
||||
cy.get(VARIANTS_SELECTORS.skuTextField).type(sku);
|
||||
cy.get(VARIANTS_SELECTORS.skuTextField).click({ force: true }).type(sku);
|
||||
}
|
||||
if (warehouseName) {
|
||||
cy.get(VARIANTS_SELECTORS.addWarehouseButton).click();
|
||||
|
@ -114,7 +113,7 @@ export function fillUpVariantAttributeAndSku({ attributeName, sku }) {
|
|||
.contains(attributeName)
|
||||
.click();
|
||||
if (sku) {
|
||||
cy.get(VARIANTS_SELECTORS.skuTextField).type(sku);
|
||||
cy.get(VARIANTS_SELECTORS.skuTextField).click({ force: true }).type(sku);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ function updateProductManageInChannel(productUrl, manageSelector) {
|
|||
.get(AVAILABLE_CHANNELS_FORM.assignedChannels)
|
||||
.click()
|
||||
.get(manageSelector)
|
||||
.first()
|
||||
.click()
|
||||
.waitForProgressBarToNotBeVisible()
|
||||
.addAliasToGraphRequest("ProductChannelListingUpdate")
|
||||
|
|
|
@ -80,7 +80,9 @@ export function selectProductsOutOfStock() {
|
|||
}
|
||||
|
||||
export function selectFilterBy(filter) {
|
||||
return showFilters().get(PRODUCTS_LIST.filters.filterBy[filter]).click();
|
||||
return showFilters()
|
||||
.get(PRODUCTS_LIST.filters.filterBy[filter])
|
||||
.click({ timeout: 1000, force: true });
|
||||
}
|
||||
|
||||
export function selectFilterByAttribute(attributeSlug) {
|
||||
|
@ -106,6 +108,8 @@ export function selectChannel(channelSlug) {
|
|||
export function submitFilters() {
|
||||
cy.addAliasToGraphRequest("ProductList")
|
||||
.get(BUTTON_SELECTORS.submit)
|
||||
.scrollIntoView()
|
||||
.should("be.visible")
|
||||
.click()
|
||||
.waitForRequestAndCheckIfNoErrors("@ProductList")
|
||||
.get(PRODUCTS_LIST.emptyProductRow)
|
||||
|
|
|
@ -26,9 +26,11 @@ export function fillUpShippingZoneData({
|
|||
channelName,
|
||||
}) {
|
||||
cy.get(SHIPPING_ZONE_DETAILS.nameInput)
|
||||
.clearAndType(shippingName)
|
||||
.clear()
|
||||
.type(shippingName)
|
||||
.get(SHIPPING_ZONE_DETAILS.descriptionInput)
|
||||
.clearAndType(shippingName)
|
||||
.clear()
|
||||
.type(shippingName)
|
||||
.get(BUTTON_SELECTORS.confirm)
|
||||
.click()
|
||||
.confirmationMessageShouldAppear()
|
||||
|
|
|
@ -21,7 +21,9 @@ export function updateTranslationToCategory({
|
|||
.get(SHARED_ELEMENTS.skeleton)
|
||||
.should("not.exist")
|
||||
.get(ELEMENT_TRANSLATION.translationInputField)
|
||||
.clearAndType(translatedName)
|
||||
.clear()
|
||||
.type(translatedName)
|
||||
// .clearAndType(translatedName)
|
||||
.get(BUTTON_SELECTORS.confirm)
|
||||
.click()
|
||||
.confirmationMessageShouldDisappear()
|
||||
|
@ -37,14 +39,17 @@ export function updateTranslationToCategory({
|
|||
.get(ELEMENT_TRANSLATION.editSeoTitleButton)
|
||||
.click()
|
||||
.get(ELEMENT_TRANSLATION.translationInputField)
|
||||
.clearAndType(translatedSeoTitle)
|
||||
.clear()
|
||||
.type(translatedSeoTitle)
|
||||
.get(BUTTON_SELECTORS.confirm)
|
||||
.click()
|
||||
.confirmationMessageShouldDisappear()
|
||||
.get(ELEMENT_TRANSLATION.editSeoDescriptionButton)
|
||||
.click()
|
||||
.get(ELEMENT_TRANSLATION.translationInputField)
|
||||
.clearAndType(translatedSeoDescription)
|
||||
.clear()
|
||||
.type(translatedSeoDescription)
|
||||
// .clearAndType(translatedSeoDescription)
|
||||
.get(BUTTON_SELECTORS.confirm)
|
||||
.click()
|
||||
.confirmationMessageShouldDisappear();
|
||||
|
|
|
@ -1614,6 +1614,10 @@
|
|||
"context": "area units type",
|
||||
"string": "Area"
|
||||
},
|
||||
"AD1PlC": {
|
||||
"context": "channels availability text",
|
||||
"string": "In {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}"
|
||||
},
|
||||
"ADTNND": {
|
||||
"context": "product type",
|
||||
"string": "Physical"
|
||||
|
@ -8100,10 +8104,6 @@
|
|||
"context": "voucher discount type",
|
||||
"string": "Fixed Amount"
|
||||
},
|
||||
"vY2lpx": {
|
||||
"context": "channels availability text",
|
||||
"string": "Available at {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}"
|
||||
},
|
||||
"vZMs8f": {
|
||||
"context": "default product variant indicator",
|
||||
"string": "Default"
|
||||
|
@ -8497,6 +8497,9 @@
|
|||
"context": "voucher requirements, header",
|
||||
"string": "Minimum Requirements"
|
||||
},
|
||||
"yi1HSj": {
|
||||
"string": "({numberOfCharacters} of {maxCharacters} characters)"
|
||||
},
|
||||
"ymo+cm": {
|
||||
"context": "voucher discount",
|
||||
"string": "All products"
|
||||
|
|
61
package-lock.json
generated
61
package-lock.json
generated
|
@ -27,7 +27,7 @@
|
|||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@material-ui/styles": "^4.11.4",
|
||||
"@reach/auto-id": "^0.16.0",
|
||||
"@saleor/macaw-ui": "0.8.0-pre.68",
|
||||
"@saleor/macaw-ui": "0.8.0-pre.73",
|
||||
"@saleor/sdk": "^0.5.0",
|
||||
"@sentry/react": "^6.0.0",
|
||||
"@types/faker": "^5.1.6",
|
||||
|
@ -7976,9 +7976,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@saleor/macaw-ui": {
|
||||
"version": "0.8.0-pre.68",
|
||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.68.tgz",
|
||||
"integrity": "sha512-4P1Ec4dpNk0MyejtosBZLd0EHl3pOBY1aFt09/R0Qpdiyu2+O7Jh/O1pwG+cpaG7J6hcrtMGsfYfLl24PwIAAA==",
|
||||
"version": "0.8.0-pre.73",
|
||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.73.tgz",
|
||||
"integrity": "sha512-7+bFDATTV8ZjX3dpU20z5QMUMSZHoNgidLWEzUBkHWk6EgiDf+V5Mn2t8Eexd34uLOIKCQ0j4f/mVG+slRsj/w==",
|
||||
"dependencies": {
|
||||
"@dessert-box/react": "^0.4.0",
|
||||
"@floating-ui/react-dom-interactions": "^0.5.0",
|
||||
|
@ -7992,14 +7992,17 @@
|
|||
"@radix-ui/react-select": "^1.2.0",
|
||||
"@radix-ui/react-toggle": "^1.0.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.5",
|
||||
"@vanilla-extract/css-utils": "^0.1.3",
|
||||
"clsx": "^1.1.1",
|
||||
"downshift": "^6.1.7",
|
||||
"downshift7": "npm:downshift@7.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react-inlinesvg": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 <19"
|
||||
"node": ">=16 <19",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
|
@ -16986,6 +16989,27 @@
|
|||
"react": ">=16.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/downshift7": {
|
||||
"name": "downshift",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.0.tgz",
|
||||
"integrity": "sha512-VSoTVynTAsabou/hbZ6HJHUVhtBiVOjQoBsCPcQq5eAROIGP+9XKMp9asAKQ3cEcUP4oe0fFdD2pziUjhFY33Q==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.8",
|
||||
"compute-scroll-into-view": "^2.0.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^17.0.2",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/downshift7/node_modules/compute-scroll-into-view": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
|
||||
"integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
|
||||
},
|
||||
"node_modules/duplexer": {
|
||||
"version": "0.1.2",
|
||||
"devOptional": true,
|
||||
|
@ -43347,9 +43371,9 @@
|
|||
}
|
||||
},
|
||||
"@saleor/macaw-ui": {
|
||||
"version": "0.8.0-pre.68",
|
||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.68.tgz",
|
||||
"integrity": "sha512-4P1Ec4dpNk0MyejtosBZLd0EHl3pOBY1aFt09/R0Qpdiyu2+O7Jh/O1pwG+cpaG7J6hcrtMGsfYfLl24PwIAAA==",
|
||||
"version": "0.8.0-pre.73",
|
||||
"resolved": "https://registry.npmjs.org/@saleor/macaw-ui/-/macaw-ui-0.8.0-pre.73.tgz",
|
||||
"integrity": "sha512-7+bFDATTV8ZjX3dpU20z5QMUMSZHoNgidLWEzUBkHWk6EgiDf+V5Mn2t8Eexd34uLOIKCQ0j4f/mVG+slRsj/w==",
|
||||
"requires": {
|
||||
"@dessert-box/react": "^0.4.0",
|
||||
"@floating-ui/react-dom-interactions": "^0.5.0",
|
||||
|
@ -43363,8 +43387,10 @@
|
|||
"@radix-ui/react-select": "^1.2.0",
|
||||
"@radix-ui/react-toggle": "^1.0.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.5",
|
||||
"@vanilla-extract/css-utils": "^0.1.3",
|
||||
"clsx": "^1.1.1",
|
||||
"downshift": "^6.1.7",
|
||||
"downshift7": "npm:downshift@7.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react-inlinesvg": "^3.0.1"
|
||||
|
@ -49697,6 +49723,25 @@
|
|||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"downshift7": {
|
||||
"version": "npm:downshift@7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.0.tgz",
|
||||
"integrity": "sha512-VSoTVynTAsabou/hbZ6HJHUVhtBiVOjQoBsCPcQq5eAROIGP+9XKMp9asAKQ3cEcUP4oe0fFdD2pziUjhFY33Q==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.14.8",
|
||||
"compute-scroll-into-view": "^2.0.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^17.0.2",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"compute-scroll-into-view": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-2.0.4.tgz",
|
||||
"integrity": "sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"duplexer": {
|
||||
"version": "0.1.2",
|
||||
"devOptional": true
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@material-ui/styles": "^4.11.4",
|
||||
"@reach/auto-id": "^0.16.0",
|
||||
"@saleor/macaw-ui": "0.8.0-pre.68",
|
||||
"@saleor/macaw-ui": "0.8.0-pre.73",
|
||||
"@saleor/sdk": "^0.5.0",
|
||||
"@sentry/react": "^6.0.0",
|
||||
"@types/faker": "^5.1.6",
|
||||
|
|
|
@ -4,7 +4,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
|||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
||||
import { MetadataFormData } from "@dashboard/components/Metadata/types";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import { ListSettingsUpdate } from "@dashboard/components/TablePagination";
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||
import { CardSpacer } from "@dashboard/components/CardSpacer";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import SeoForm from "@dashboard/components/SeoForm";
|
||||
import { SeoForm } from "@dashboard/components/SeoForm";
|
||||
import { ProductErrorFragment } from "@dashboard/graphql";
|
||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||
|
|
|
@ -8,9 +8,9 @@ import { Button } from "@dashboard/components/Button";
|
|||
import { CardSpacer } from "@dashboard/components/CardSpacer";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import SeoForm from "@dashboard/components/SeoForm";
|
||||
import { SeoForm } from "@dashboard/components/SeoForm";
|
||||
import { Tab, TabContainer } from "@dashboard/components/Tab";
|
||||
import { CategoryDetailsQuery, ProductErrorFragment } from "@dashboard/graphql";
|
||||
import { SubmitPromise } from "@dashboard/hooks/useForm";
|
||||
|
|
|
@ -4,9 +4,9 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
|||
import { CardSpacer } from "@dashboard/components/CardSpacer";
|
||||
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import SeoForm from "@dashboard/components/SeoForm";
|
||||
import { SeoForm } from "@dashboard/components/SeoForm";
|
||||
import {
|
||||
CollectionChannelListingErrorFragment,
|
||||
CollectionErrorFragment,
|
||||
|
|
|
@ -4,9 +4,9 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
|||
import { CardSpacer } from "@dashboard/components/CardSpacer";
|
||||
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import SeoForm from "@dashboard/components/SeoForm";
|
||||
import { SeoForm } from "@dashboard/components/SeoForm";
|
||||
import {
|
||||
CollectionChannelListingErrorFragment,
|
||||
CollectionDetailsQuery,
|
||||
|
|
|
@ -19,10 +19,7 @@ interface TopNavMenuProps {
|
|||
dataTestId?: string;
|
||||
}
|
||||
|
||||
export const TopNavMenu: React.FC<TopNavMenuProps> = ({
|
||||
items,
|
||||
dataTestId,
|
||||
}) => (
|
||||
export const Menu: React.FC<TopNavMenuProps> = ({ items, dataTestId }) => (
|
||||
<Dropdown data-test-id={dataTestId}>
|
||||
<Dropdown.Trigger>
|
||||
<Button
|
|
@ -13,7 +13,7 @@ interface TopNavProps {
|
|||
isAlignToRight?: boolean;
|
||||
}
|
||||
|
||||
export const TopNav: React.FC<PropsWithChildren<TopNavProps>> = ({
|
||||
export const Root: React.FC<PropsWithChildren<TopNavProps>> = ({
|
||||
title,
|
||||
href,
|
||||
withoutBorder = false,
|
|
@ -1,3 +1,8 @@
|
|||
export * from "./TopNav";
|
||||
import { Menu } from "./Menu";
|
||||
import { Root } from "./Root";
|
||||
export * from "./TopNavLink";
|
||||
export * from "./TopNavWrapper";
|
||||
|
||||
export const TopNav = Object.assign(Root, {
|
||||
Menu,
|
||||
});
|
||||
|
|
33
src/components/Attributes/AttributeListItem.tsx
Normal file
33
src/components/Attributes/AttributeListItem.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
import { inputTypeMessages } from "@dashboard/attributes/components/AttributeDetails/messages";
|
||||
import { getMeasurementUnitMessage } from "@dashboard/attributes/components/AttributeDetails/utils";
|
||||
import BasicAttributeRow from "@dashboard/components/Attributes/BasicAttributeRow";
|
||||
import { BasicAttributeRow } from "@dashboard/components/Attributes/BasicAttributeRow";
|
||||
import ExtendedAttributeRow from "@dashboard/components/Attributes/ExtendedAttributeRow";
|
||||
import { attributeRowMessages } from "@dashboard/components/Attributes/messages";
|
||||
import { SwatchRow } from "@dashboard/components/Attributes/SwatchRow";
|
||||
|
@ -13,8 +12,6 @@ import {
|
|||
getSingleChoices,
|
||||
getSingleDisplayValue,
|
||||
} from "@dashboard/components/Attributes/utils";
|
||||
import Checkbox from "@dashboard/components/Checkbox";
|
||||
import { DateTimeField } from "@dashboard/components/DateTimeField";
|
||||
import FileUploadField from "@dashboard/components/FileUploadField";
|
||||
import MultiAutocompleteSelectField from "@dashboard/components/MultiAutocompleteSelectField";
|
||||
import RichTextEditor from "@dashboard/components/RichTextEditor";
|
||||
|
@ -22,11 +19,12 @@ import SingleAutocompleteSelectField from "@dashboard/components/SingleAutocompl
|
|||
import SortableChipsField from "@dashboard/components/SortableChipsField";
|
||||
import { AttributeInputTypeEnum } from "@dashboard/graphql";
|
||||
import { commonMessages } from "@dashboard/intl";
|
||||
import { InputAdornment, TextField } from "@material-ui/core";
|
||||
import { TextField } from "@material-ui/core";
|
||||
import { Box, Checkbox, Input, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { useStyles } from "./styles";
|
||||
import { DateTimeField } from "../DateTimeField";
|
||||
import { AttributeRowProps } from "./types";
|
||||
|
||||
const AttributeRow: React.FC<AttributeRowProps> = ({
|
||||
|
@ -47,7 +45,6 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
richTextGetters,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles();
|
||||
|
||||
switch (attribute.data.inputType) {
|
||||
case AttributeInputTypeEnum.REFERENCE:
|
||||
|
@ -77,7 +74,6 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
return (
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
<FileUploadField
|
||||
className={classes.fileField}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
file={getFileChoice(attribute)}
|
||||
|
@ -93,7 +89,10 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
);
|
||||
case AttributeInputTypeEnum.DROPDOWN:
|
||||
return (
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
<BasicAttributeRow
|
||||
label={attribute.label}
|
||||
id={`attribute:${attribute.label}`}
|
||||
>
|
||||
<SingleAutocompleteSelectField
|
||||
choices={getSingleChoices(attributeValues)}
|
||||
disabled={disabled}
|
||||
|
@ -102,6 +101,7 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
id={`attribute:${attribute.label}`}
|
||||
label={intl.formatMessage(attributeRowMessages.valueLabel)}
|
||||
value={attribute.value[0]}
|
||||
onChange={event => onChange(attribute.id, event.target.value)}
|
||||
|
@ -130,18 +130,19 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
<BasicAttributeRow
|
||||
label={attribute.label}
|
||||
description={intl.formatMessage(inputTypeMessages.plainText)}
|
||||
id={`attribute:${attribute.label}`}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
<Input
|
||||
disabled={disabled}
|
||||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
label={intl.formatMessage(attributeRowMessages.valueLabel)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
onChange={event => onChange(attribute.id, event.target.value)}
|
||||
type="text"
|
||||
value={attribute.value[0]}
|
||||
size="small"
|
||||
id={`attribute:${attribute.label}`}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
/>
|
||||
</BasicAttributeRow>
|
||||
);
|
||||
|
@ -157,70 +158,85 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
<BasicAttributeRow
|
||||
label={attribute.label}
|
||||
description={intl.formatMessage(inputTypeMessages.richText)}
|
||||
id={`attribute:${attribute.label}`}
|
||||
>
|
||||
{getShouldMount(attribute.id) && (
|
||||
<RichTextEditor
|
||||
defaultValue={defaultValue}
|
||||
editorRef={getMountEditor(attribute.id)}
|
||||
onChange={getHandleChange(attribute.id)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
disabled={disabled}
|
||||
error={!!error}
|
||||
label={intl.formatMessage(attributeRowMessages.valueLabel)}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
/>
|
||||
<Box __minWidth={210}>
|
||||
<RichTextEditor
|
||||
defaultValue={defaultValue}
|
||||
editorRef={getMountEditor(attribute.id)}
|
||||
onChange={getHandleChange(attribute.id)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
disabled={disabled}
|
||||
error={!!error}
|
||||
label={intl.formatMessage(attributeRowMessages.valueLabel)}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
id={`attribute:${attribute.label}`}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</BasicAttributeRow>
|
||||
);
|
||||
case AttributeInputTypeEnum.NUMERIC:
|
||||
return (
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
<TextField
|
||||
fullWidth
|
||||
<BasicAttributeRow
|
||||
label={attribute.label}
|
||||
id={`attribute:${attribute.label}`}
|
||||
>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
label={intl.formatMessage(attributeRowMessages.valueLabel)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
id={`attribute:${attribute.label}`}
|
||||
onChange={event => onChange(attribute.id, event.target.value)}
|
||||
type="number"
|
||||
value={attribute.value[0]}
|
||||
InputProps={
|
||||
attribute.data.unit && {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{getMeasurementUnitMessage(
|
||||
attribute.data.unit,
|
||||
intl.formatMessage,
|
||||
)}
|
||||
</InputAdornment>
|
||||
),
|
||||
}
|
||||
}
|
||||
size="small"
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
/>
|
||||
</BasicAttributeRow>
|
||||
);
|
||||
case AttributeInputTypeEnum.BOOLEAN:
|
||||
return (
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
<div className={classes.pullRight}>
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
name={`attribute:${attribute.label}`}
|
||||
onChange={event =>
|
||||
onChange(attribute.id, JSON.stringify(event.target.checked))
|
||||
}
|
||||
checked={JSON.parse(attribute.value[0] ?? "false")}
|
||||
className={classes.pullRight}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
error={!!error}
|
||||
/>
|
||||
</div>
|
||||
</BasicAttributeRow>
|
||||
<Box as="li" display="flex" gap={5} alignItems="center" padding={3}>
|
||||
<Box data-test-id="attribute-value">
|
||||
<Box
|
||||
display="flex"
|
||||
gap={2}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Checkbox
|
||||
name={`attribute:${attribute.label}`}
|
||||
onCheckedChange={checked => onChange(attribute.id, checked)}
|
||||
checked={JSON.parse(attribute.value[0] ?? "false")}
|
||||
error={!!error}
|
||||
id={`attribute:${attribute.label}`}
|
||||
/>
|
||||
<Text variant="caption" color="textCriticalDefault">
|
||||
{getErrorMessage(error, intl)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
data-test-id="attribute-label"
|
||||
as="label"
|
||||
htmlFor={`attribute:${attribute.label}`}
|
||||
display="flex"
|
||||
gap={3}
|
||||
cursor="pointer"
|
||||
>
|
||||
<Text>{attribute.label}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
case AttributeInputTypeEnum.DATE:
|
||||
return (
|
||||
<BasicAttributeRow label={attribute.label} flexValueContainer>
|
||||
<BasicAttributeRow
|
||||
label={attribute.label}
|
||||
id={`attribute:${attribute.label}`}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
|
@ -228,6 +244,7 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
helperText={getErrorMessage(error, intl)}
|
||||
label={intl.formatMessage(commonMessages.date)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
id={`attribute:${attribute.label}`}
|
||||
onChange={event => onChange(attribute.id, event.target.value)}
|
||||
type="date"
|
||||
value={attribute.value[0]}
|
||||
|
@ -237,7 +254,7 @@ const AttributeRow: React.FC<AttributeRowProps> = ({
|
|||
);
|
||||
case AttributeInputTypeEnum.DATE_TIME:
|
||||
return (
|
||||
<BasicAttributeRow label={attribute.label} flexValueContainer>
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
<DateTimeField
|
||||
fullWidth
|
||||
name={`attribute:${attribute.label}`}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { AttributeReference } from "@dashboard/attributes/utils/data";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import {
|
||||
AttributeEntityTypeEnum,
|
||||
AttributeInputTypeEnum,
|
||||
|
@ -13,13 +11,12 @@ import {
|
|||
import { FormsetAtomicData } from "@dashboard/hooks/useFormset";
|
||||
import { FetchMoreProps } from "@dashboard/types";
|
||||
import { RichTextGetters } from "@dashboard/utils/richText/useMultipleRichText";
|
||||
import { Card, CardContent, Typography } from "@material-ui/core";
|
||||
import { ChevronIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import { Accordion, Box, Divider, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import AttributeRow from "./AttributeRow";
|
||||
import { DashboardCard } from "../Card";
|
||||
import { AttributeListItem } from "./AttributeListItem";
|
||||
import { AttributeRowHandlers, VariantAttributeScope } from "./types";
|
||||
|
||||
export interface AttributeInputData {
|
||||
|
@ -49,61 +46,6 @@ export interface AttributesProps extends AttributeRowHandlers {
|
|||
richTextGetters: RichTextGetters<string>;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
attributeSection: {
|
||||
"&:last-of-type": {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
padding: theme.spacing(2, 0),
|
||||
},
|
||||
attributeSectionLabel: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
},
|
||||
card: {
|
||||
overflow: "visible",
|
||||
},
|
||||
cardContent: {
|
||||
"&:last-child": {
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
paddingTop: theme.spacing(1),
|
||||
},
|
||||
expansionBar: {
|
||||
display: "flex",
|
||||
},
|
||||
expansionBarButton: {
|
||||
padding: 4,
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
expansionBarButtonIcon: {
|
||||
transition: theme.transitions.duration.short + "ms",
|
||||
},
|
||||
expansionBarLabel: {
|
||||
color: theme.palette.text.disabled,
|
||||
fontSize: 14,
|
||||
},
|
||||
expansionBarLabelContainer: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
},
|
||||
rotate: {
|
||||
transform: "rotate(180deg)",
|
||||
},
|
||||
uploadFileButton: {
|
||||
float: "right",
|
||||
},
|
||||
uploadFileContent: {
|
||||
color: theme.palette.primary.main,
|
||||
float: "right",
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
},
|
||||
}),
|
||||
{ name: "Attributes" },
|
||||
);
|
||||
|
||||
const messages = defineMessages({
|
||||
attributesNumber: {
|
||||
id: "z0gGP+",
|
||||
|
@ -117,7 +59,7 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
const Attributes: React.FC<AttributesProps> = ({
|
||||
export const Attributes: React.FC<AttributesProps> = ({
|
||||
attributes,
|
||||
attributeValues,
|
||||
errors,
|
||||
|
@ -127,66 +69,50 @@ const Attributes: React.FC<AttributesProps> = ({
|
|||
...props
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
const [expanded, setExpansionStatus] = React.useState(true);
|
||||
const toggleExpansion = () => setExpansionStatus(!expanded);
|
||||
|
||||
return (
|
||||
<Card className={classes.card}>
|
||||
<CardTitle title={title || intl.formatMessage(messages.header)} />
|
||||
<CardContent className={classes.cardContent}>
|
||||
<div className={classes.expansionBar}>
|
||||
<div className={classes.expansionBarLabelContainer}>
|
||||
<Typography className={classes.expansionBarLabel} variant="caption">
|
||||
<FormattedMessage
|
||||
{...messages.attributesNumber}
|
||||
values={{
|
||||
number: attributes.length,
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</div>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
hoverOutline={false}
|
||||
className={classes.expansionBarButton}
|
||||
onClick={toggleExpansion}
|
||||
data-test-id="attributes-expand"
|
||||
>
|
||||
<ChevronIcon
|
||||
className={clsx(classes.expansionBarButtonIcon, {
|
||||
[classes.rotate]: expanded,
|
||||
})}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
{expanded && attributes.length > 0 && (
|
||||
<>
|
||||
<Hr />
|
||||
{attributes.map((attribute, attributeIndex) => {
|
||||
const error = errors.find(err =>
|
||||
err.attributes?.includes(attribute.id),
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={attribute.id}>
|
||||
{attributeIndex > 0 && <Hr />}
|
||||
<AttributeRow
|
||||
attribute={attribute}
|
||||
attributeValues={attributeValues}
|
||||
error={error}
|
||||
onAttributeSelectBlur={onAttributeSelectBlur}
|
||||
richTextGetters={richTextGetters}
|
||||
{...props}
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
{title || intl.formatMessage(messages.header)}
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content>
|
||||
<Box display="flex" flexDirection="column" gap={5}>
|
||||
<Accordion defaultValue="attributes-accordion">
|
||||
<Accordion.Item value="attributes-accordion">
|
||||
<Accordion.Item.Trigger buttonDataTestId="attributes-expand">
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage
|
||||
{...messages.attributesNumber}
|
||||
values={{
|
||||
number: attributes.length,
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Text>
|
||||
</Accordion.Item.Trigger>
|
||||
<Accordion.Item.Content>
|
||||
{attributes.length > 0 && (
|
||||
<ul>
|
||||
<Divider />
|
||||
{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;
|
||||
|
|
|
@ -1,58 +1,58 @@
|
|||
import Grid from "@dashboard/components/Grid";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import HelpOutline from "@material-ui/icons/HelpOutline";
|
||||
import { Tooltip } from "@saleor/macaw-ui/next";
|
||||
import clsx from "clsx";
|
||||
import { Box, InfoIcon, sprinkles, Text, Tooltip } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
import { useBasicAttributeStyles } from "./styles";
|
||||
|
||||
interface BasicAttributeRowProps {
|
||||
label: string | React.ReactNode;
|
||||
description?: string | React.ReactNode;
|
||||
flexValueContainer?: boolean;
|
||||
id?: string;
|
||||
clickableLabel?: boolean;
|
||||
}
|
||||
|
||||
const BasicAttributeRow: React.FC<BasicAttributeRowProps> = ({
|
||||
export const BasicAttributeRow: React.FC<BasicAttributeRowProps> = ({
|
||||
label,
|
||||
description,
|
||||
children,
|
||||
flexValueContainer,
|
||||
}) => {
|
||||
const classes = useBasicAttributeStyles();
|
||||
|
||||
return (
|
||||
<Grid className={classes.attributeSection} variant="uniform">
|
||||
<div
|
||||
className={classes.attributeSectionLabel}
|
||||
data-test-id="attribute-label"
|
||||
>
|
||||
<Typography>
|
||||
{label}
|
||||
{description && (
|
||||
<Tooltip>
|
||||
<Tooltip.Trigger>
|
||||
<HelpOutline className={classes.tooltipIcon} />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow />
|
||||
{description}
|
||||
</Tooltip.Content>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
data-test-id="attribute-value"
|
||||
className={clsx(classes.value, {
|
||||
[classes.flex]: flexValueContainer,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
BasicAttributeRow.displayName = "BasicAttributeRow";
|
||||
export default BasicAttributeRow;
|
||||
id,
|
||||
clickableLabel = false,
|
||||
}) => (
|
||||
<Box
|
||||
as="li"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
paddingY={3}
|
||||
paddingX={2}
|
||||
display="grid"
|
||||
gridTemplateColumns={2}
|
||||
gap={8}
|
||||
>
|
||||
<Box
|
||||
data-test-id="attribute-label"
|
||||
as="label"
|
||||
htmlFor={id}
|
||||
display="flex"
|
||||
gap={3}
|
||||
cursor={clickableLabel ? "pointer" : "auto"}
|
||||
>
|
||||
<Text>{label}</Text>
|
||||
{description && (
|
||||
<Tooltip>
|
||||
<Tooltip.Trigger>
|
||||
<Box>
|
||||
<InfoIcon
|
||||
size="small"
|
||||
className={sprinkles({
|
||||
display: "block",
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="top">
|
||||
<Tooltip.Arrow />
|
||||
{description}
|
||||
</Tooltip.Content>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
<Box data-test-id="attribute-value">{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import Grid from "@dashboard/components/Grid";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
import { useExtendedAttributeStyles } from "./styles";
|
||||
|
||||
interface ExtendedAttributeRowProps {
|
||||
label: string;
|
||||
selectLabel: string;
|
||||
|
@ -14,30 +10,28 @@ interface ExtendedAttributeRowProps {
|
|||
|
||||
const ExtendedAttributeRow: React.FC<ExtendedAttributeRowProps> = props => {
|
||||
const { label, selectLabel, disabled, onSelect, children } = props;
|
||||
const classes = useExtendedAttributeStyles(props);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid className={classes.attributeSection} variant="uniform">
|
||||
<div
|
||||
className={classes.attributeSectionLabel}
|
||||
data-test-id="attribute-label"
|
||||
<Box
|
||||
as="li"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
paddingY={3}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<div data-test-id="attribute-selector">
|
||||
<Button
|
||||
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>
|
||||
{selectLabel}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box data-test-id="attribute-value">{children}</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import BasicAttributeRow from "@dashboard/components/Attributes/BasicAttributeRow";
|
||||
import { BasicAttributeRow } from "@dashboard/components/Attributes/BasicAttributeRow";
|
||||
import {
|
||||
getErrorMessage,
|
||||
getSingleDisplayValue,
|
||||
|
@ -38,7 +38,10 @@ export const SwatchRow: React.FC<SwatchRowProps> = ({
|
|||
const value = attribute.data.values.find(getBySlug(attribute.value[0]));
|
||||
|
||||
return (
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
<BasicAttributeRow
|
||||
label={attribute.label}
|
||||
id={`attribute:${attribute.label}`}
|
||||
>
|
||||
<SingleAutocompleteSelectField
|
||||
fetchOnFocus
|
||||
allowCustomValues={false}
|
||||
|
@ -65,6 +68,7 @@ export const SwatchRow: React.FC<SwatchRowProps> = ({
|
|||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
id={`attribute:${attribute.label}`}
|
||||
value={attribute.value[0]}
|
||||
onChange={event => onChange(attribute.id, event.target.value)}
|
||||
fetchChoices={value => fetchAttributeValues(value, attribute.id)}
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
export { default } from "./Attributes";
|
||||
export * from "./Attributes";
|
||||
export * from "./types";
|
||||
|
|
|
@ -2,13 +2,6 @@ import { makeStyles } from "@saleor/macaw-ui";
|
|||
|
||||
export const useStyles = makeStyles(
|
||||
() => ({
|
||||
fileField: {
|
||||
float: "right",
|
||||
},
|
||||
pullRight: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
swatchInput: {
|
||||
paddingTop: 16.5,
|
||||
paddingBottom: 16.5,
|
||||
|
@ -23,62 +16,3 @@ export const useStyles = makeStyles(
|
|||
}),
|
||||
{ name: "AttributeRow" },
|
||||
);
|
||||
|
||||
export const useBasicAttributeStyles = makeStyles(
|
||||
theme => ({
|
||||
attributeSection: {
|
||||
"&:last-of-type": {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
padding: theme.spacing(2, 0),
|
||||
wordBreak: "break-word",
|
||||
},
|
||||
attributeSectionLabel: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
},
|
||||
flex: {
|
||||
columnGap: theme.spacing(2) + "px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
[theme.breakpoints.down("md")]: {
|
||||
flexDirection: "column",
|
||||
rowGap: theme.spacing(2) + "px",
|
||||
},
|
||||
},
|
||||
value: {
|
||||
"&&": {
|
||||
overflow: "visible",
|
||||
},
|
||||
},
|
||||
tooltipIcon: {
|
||||
fill: theme.palette.type === "dark" ? "#FAFAFA" : "#28234A",
|
||||
fillOpacity: 0.6,
|
||||
"&:hover": {
|
||||
fillOpacity: 1,
|
||||
},
|
||||
padding: theme.spacing(0.25),
|
||||
marginLeft: theme.spacing(0.75),
|
||||
},
|
||||
}),
|
||||
{ name: "BasicAttributeRow" },
|
||||
);
|
||||
|
||||
export const useExtendedAttributeStyles = makeStyles(
|
||||
theme => ({
|
||||
attributeSection: {
|
||||
"&:last-of-type": {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
padding: theme.spacing(2, 0),
|
||||
},
|
||||
attributeSectionButton: {
|
||||
float: "right",
|
||||
},
|
||||
attributeSectionLabel: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
},
|
||||
}),
|
||||
{ name: "ExtendedAttributeRow" },
|
||||
);
|
||||
|
|
|
@ -16,7 +16,7 @@ export enum VariantAttributeScope {
|
|||
}
|
||||
|
||||
export interface AttributeRowHandlers {
|
||||
onChange: FormsetChange<string>;
|
||||
onChange: FormsetChange<string | boolean>;
|
||||
onFileChange: FormsetChange<File>;
|
||||
onMultiChange: FormsetChange<string>;
|
||||
onReferencesAddClick: (attribute: AttributeInput) => void;
|
||||
|
|
8
src/components/Card/Content.tsx
Normal file
8
src/components/Card/Content.tsx
Normal 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>
|
||||
);
|
8
src/components/Card/Root.tsx
Normal file
8
src/components/Card/Root.tsx
Normal 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>
|
||||
);
|
8
src/components/Card/Title.tsx
Normal file
8
src/components/Card/Title.tsx
Normal 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>
|
||||
);
|
5
src/components/Card/index.ts
Normal file
5
src/components/Card/index.ts
Normal 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 });
|
|
@ -1,16 +1,18 @@
|
|||
import { ChannelData } from "@dashboard/channels/utils";
|
||||
import ControlledCheckbox from "@dashboard/components/ControlledCheckbox";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import RadioSwitchField from "@dashboard/components/RadioSwitchField";
|
||||
import useCurrentDate from "@dashboard/hooks/useCurrentDate";
|
||||
import useDateLocalize from "@dashboard/hooks/useDateLocalize";
|
||||
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
|
||||
import { TextField, Typography } from "@material-ui/core";
|
||||
import clsx from "clsx";
|
||||
import { TextField } from "@material-ui/core";
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
Divider,
|
||||
RadioGroup,
|
||||
Text,
|
||||
} from "@saleor/macaw-ui/next";
|
||||
import React, { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { useStyles } from "../styles";
|
||||
import { ChannelOpts, ChannelsAvailabilityError, Messages } from "../types";
|
||||
import { availabilityItemMessages } from "./messages";
|
||||
|
||||
|
@ -22,7 +24,7 @@ export interface ChannelContentProps {
|
|||
onChange: (id: string, data: ChannelOpts) => void;
|
||||
}
|
||||
|
||||
const ChannelContent: React.FC<ChannelContentProps> = ({
|
||||
export const ChannelAvailabilityItemContent: React.FC<ChannelContentProps> = ({
|
||||
data,
|
||||
disabled,
|
||||
errors,
|
||||
|
@ -55,7 +57,6 @@ const ChannelContent: React.FC<ChannelContentProps> = ({
|
|||
);
|
||||
const [isAvailableDate, setAvailableDate] = useState(false);
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
|
||||
const parsedDate = new Date(dateNow);
|
||||
const todayDateUTC = parsedDate.toISOString().slice(0, 10);
|
||||
|
@ -70,57 +71,64 @@ const ChannelContent: React.FC<ChannelContentProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<RadioSwitchField
|
||||
classes={{
|
||||
radioLabel: classes.radioLabel,
|
||||
}}
|
||||
className={classes.radioField}
|
||||
disabled={disabled}
|
||||
firstOptionLabel={
|
||||
<>
|
||||
<p className={classes.label}>{messages.visibleLabel}</p>
|
||||
{isPublished &&
|
||||
publicationDate &&
|
||||
Date.parse(publicationDate) < dateNow && (
|
||||
<span className={classes.secondLabel}>
|
||||
{messages.visibleSecondLabel ||
|
||||
visibleMessage(publicationDate)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
name="isPublished"
|
||||
secondOptionLabel={
|
||||
<>
|
||||
<p className={classes.label}>{messages.hiddenLabel}</p>
|
||||
{publicationDate &&
|
||||
!isPublished &&
|
||||
Date.parse(publicationDate) >= dateNow && (
|
||||
<span className={classes.secondLabel}>
|
||||
{messages.hiddenSecondLabel}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
value={isPublished}
|
||||
onChange={() => {
|
||||
<Box display="flex" gap={6} paddingTop={6} flexDirection="column">
|
||||
<RadioGroup
|
||||
value={String(isPublished)}
|
||||
onValueChange={value => {
|
||||
onChange(id, {
|
||||
...formData,
|
||||
isPublished: !isPublished,
|
||||
isPublished: value === "true",
|
||||
publicationDate:
|
||||
!isPublished && !publicationDate ? todayDateUTC : publicationDate,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
disabled={disabled}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
gap={6}
|
||||
>
|
||||
<RadioGroup.Item
|
||||
id={`${id}-isPublished-true`}
|
||||
value="true"
|
||||
name="isPublished"
|
||||
>
|
||||
<Box display="flex" alignItems="baseline" gap={5}>
|
||||
<Text>{messages.visibleLabel}</Text>
|
||||
{isPublished &&
|
||||
publicationDate &&
|
||||
Date.parse(publicationDate) < dateNow && (
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
{messages.visibleSecondLabel ||
|
||||
visibleMessage(publicationDate)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</RadioGroup.Item>
|
||||
<RadioGroup.Item
|
||||
id={`${id}-isPublished-false`}
|
||||
value="false"
|
||||
name="isPublished"
|
||||
>
|
||||
<Box display="flex" alignItems="baseline" gap={5}>
|
||||
<Text>{messages.hiddenLabel}</Text>
|
||||
{publicationDate &&
|
||||
!isPublished &&
|
||||
Date.parse(publicationDate) >= dateNow && (
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
{messages.hiddenSecondLabel}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</RadioGroup.Item>
|
||||
</RadioGroup>
|
||||
{!isPublished && (
|
||||
<>
|
||||
<Typography
|
||||
className={classes.setPublicationDate}
|
||||
onClick={() => setPublicationDate(!isPublicationDate)}
|
||||
<Box display="flex" flexDirection="column" alignItems="start" gap={3}>
|
||||
<Checkbox
|
||||
onCheckedChange={(checked: boolean) => setPublicationDate(checked)}
|
||||
checked={isPublicationDate}
|
||||
>
|
||||
{intl.formatMessage(availabilityItemMessages.setPublicationDate)}
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
{isPublicationDate && (
|
||||
<TextField
|
||||
error={!!formErrors.publicationDate}
|
||||
|
@ -141,64 +149,75 @@ const ChannelContent: React.FC<ChannelContentProps> = ({
|
|||
publicationDate: e.target.value || null,
|
||||
})
|
||||
}
|
||||
className={classes.date}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
)}
|
||||
{hasAvailableProps && (
|
||||
<>
|
||||
<Hr />
|
||||
<RadioSwitchField
|
||||
classes={{
|
||||
radioLabel: classes.radioLabel,
|
||||
}}
|
||||
className={classes.radioField}
|
||||
<Divider />
|
||||
<RadioGroup
|
||||
disabled={disabled}
|
||||
firstOptionLabel={
|
||||
<>
|
||||
<p className={classes.label}>{messages.availableLabel}</p>
|
||||
name={`channel:isAvailableForPurchase:${id}`}
|
||||
value={String(isAvailable)}
|
||||
onValueChange={value =>
|
||||
onChange(id, {
|
||||
...formData,
|
||||
availableForPurchase: !value ? null : availableForPurchase,
|
||||
isAvailableForPurchase: value === "true",
|
||||
})
|
||||
}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
gap={6}
|
||||
>
|
||||
<RadioGroup.Item
|
||||
id={`channel:isAvailableForPurchase:${id}-true`}
|
||||
value="true"
|
||||
>
|
||||
<Box display="flex" __alignItems="baseline" gap={5}>
|
||||
<Text>{messages.availableLabel}</Text>
|
||||
{isAvailable &&
|
||||
availableForPurchase &&
|
||||
Date.parse(availableForPurchase) < dateNow && (
|
||||
<span className={classes.secondLabel}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
{visibleMessage(availableForPurchase)}
|
||||
</span>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
name={`channel:isAvailableForPurchase:${id}`}
|
||||
secondOptionLabel={
|
||||
<>
|
||||
<p className={classes.label}>{messages.unavailableLabel}</p>
|
||||
</Box>
|
||||
</RadioGroup.Item>
|
||||
<RadioGroup.Item
|
||||
id={`channel:isAvailableForPurchase:${id}-false`}
|
||||
value="false"
|
||||
>
|
||||
<Box display="flex" __alignItems="baseline" gap={5}>
|
||||
<Text>{messages.unavailableLabel}</Text>
|
||||
{availableForPurchase && !isAvailable && (
|
||||
<span className={classes.secondLabel}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
{messages.availableSecondLabel}
|
||||
</span>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
value={isAvailable}
|
||||
onChange={e => {
|
||||
const { value } = e.target;
|
||||
return onChange(id, {
|
||||
...formData,
|
||||
availableForPurchase: !value ? null : availableForPurchase,
|
||||
isAvailableForPurchase: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</RadioGroup.Item>
|
||||
</RadioGroup>
|
||||
{!isAvailable && (
|
||||
<>
|
||||
<Typography
|
||||
className={classes.setPublicationDate}
|
||||
onClick={() => setAvailableDate(!isAvailableDate)}
|
||||
<Box
|
||||
display="flex"
|
||||
gap={3}
|
||||
flexDirection="column"
|
||||
alignItems="start"
|
||||
>
|
||||
<Checkbox
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setAvailableDate(checked)
|
||||
}
|
||||
checked={isAvailableDate}
|
||||
>
|
||||
{messages.setAvailabilityDateLabel}
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
{isAvailableDate && (
|
||||
<TextField
|
||||
error={!!formErrors.availableForPurchaseDate}
|
||||
|
@ -224,46 +243,41 @@ const ChannelContent: React.FC<ChannelContentProps> = ({
|
|||
availableForPurchase: e.target.value,
|
||||
})
|
||||
}
|
||||
className={classes.date}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{visibleInListings !== undefined && (
|
||||
<>
|
||||
<Hr />
|
||||
<ControlledCheckbox
|
||||
className={classes.checkbox}
|
||||
<Divider />
|
||||
<Checkbox
|
||||
name={`channel:visibleInListings:${id}`}
|
||||
id={`channel:visibleInListings:${id}`}
|
||||
checked={!visibleInListings}
|
||||
disabled={disabled}
|
||||
label={
|
||||
<>
|
||||
<p className={clsx(classes.label, classes.listingLabel)}>
|
||||
{intl.formatMessage(availabilityItemMessages.hideInListings)}
|
||||
</p>
|
||||
<span className={classes.secondLabel}>
|
||||
{intl.formatMessage(
|
||||
availabilityItemMessages.hideInListingsDescription,
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
onChange={e =>
|
||||
onCheckedChange={checked => {
|
||||
onChange(id, {
|
||||
...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;
|
||||
|
|
|
@ -1,94 +1,28 @@
|
|||
import { ChannelData } from "@dashboard/channels/utils";
|
||||
import Label from "@dashboard/orders/components/OrderHistory/Label";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { Accordion, AccordionSummary, makeStyles } from "@saleor/macaw-ui";
|
||||
import { Accordion, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
import { Messages } from "../types";
|
||||
|
||||
const useExpanderStyles = makeStyles(
|
||||
theme => ({
|
||||
expanded: {},
|
||||
root: {
|
||||
boxShadow: "none",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
paddingBottom: theme.spacing(2),
|
||||
|
||||
"&:before": {
|
||||
content: "none",
|
||||
},
|
||||
|
||||
"&$expanded": {
|
||||
margin: 0,
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ name: "ChannelContentWrapperExpander" },
|
||||
);
|
||||
|
||||
const useSummaryStyles = makeStyles(
|
||||
theme => ({
|
||||
expanded: {},
|
||||
root: {
|
||||
width: "100%",
|
||||
border: "none",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
minHeight: 0,
|
||||
paddingTop: theme.spacing(2),
|
||||
|
||||
"&$expanded": {
|
||||
minHeight: 0,
|
||||
padding: theme.spacing(2, 0),
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ name: "ChannelContentWrapperExpanderSummary" },
|
||||
);
|
||||
|
||||
const useStyles = makeStyles(
|
||||
() => ({
|
||||
container: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
}),
|
||||
{ name: "ChannelWithVariantAvailabilityItemWrapper" },
|
||||
);
|
||||
|
||||
export interface ChannelContentWrapperProps {
|
||||
data: ChannelData;
|
||||
children: React.ReactNode;
|
||||
messages: Messages;
|
||||
}
|
||||
|
||||
const ChannelContentWrapper: React.FC<ChannelContentWrapperProps> = ({
|
||||
data,
|
||||
messages,
|
||||
children,
|
||||
}) => {
|
||||
const expanderClasses = useExpanderStyles();
|
||||
const summaryClasses = useSummaryStyles();
|
||||
const classes = useStyles();
|
||||
|
||||
const { name } = data;
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
classes={expanderClasses}
|
||||
data-test-id="channel-availability-item"
|
||||
>
|
||||
<AccordionSummary className={summaryClasses.root}>
|
||||
<div className={classes.container}>
|
||||
<Typography>{name}</Typography>
|
||||
<Label text={messages.availableDateText} />
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
{children}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelContentWrapper;
|
||||
export const ChannelAvailabilityItemWrapper: React.FC<
|
||||
ChannelContentWrapperProps
|
||||
> = ({ data: { name }, messages, children }) => (
|
||||
<Accordion data-test-id="channel-availability-item">
|
||||
<Accordion.Item value="channel-availability-item" gap={12}>
|
||||
<Accordion.Item.Trigger buttonDataTestId="expand-icon">
|
||||
<Text variant={"bodyEmp"}>{name}</Text>
|
||||
<Label text={messages.availableDateText} />
|
||||
</Accordion.Item.Trigger>
|
||||
<Accordion.Item.Content paddingLeft={6}>
|
||||
{children}
|
||||
</Accordion.Item.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
|
2
src/components/ChannelsAvailabilityCard/Channel/index.ts
Normal file
2
src/components/ChannelsAvailabilityCard/Channel/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./ChannelAvailabilityItemContent";
|
||||
export * from "./ChannelAvailabilityItemWrapper";
|
|
@ -1,18 +1,19 @@
|
|||
import { Channel as ChannelList, ChannelData } from "@dashboard/channels/utils";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import { PermissionEnum } from "@dashboard/graphql";
|
||||
import useDateLocalize from "@dashboard/hooks/useDateLocalize";
|
||||
import { RequireOnlyOne } from "@dashboard/misc";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { Box, Divider, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import ChannelAvailabilityItemContent from "./Channel/ChannelAvailabilityItemContent";
|
||||
import ChannelAvailabilityItemWrapper from "./Channel/ChannelAvailabilityItemWrapper";
|
||||
import ChannelsAvailabilityCardWrapper, {
|
||||
import {
|
||||
ChannelAvailabilityItemContent,
|
||||
ChannelAvailabilityItemWrapper,
|
||||
} from "./Channel";
|
||||
import {
|
||||
ChannelsAvailabilityCardWrapper,
|
||||
ChannelsAvailabilityWrapperProps,
|
||||
} from "./ChannelsAvailabilityCardWrapper";
|
||||
import { useStyles } from "./styles";
|
||||
import { ChannelOpts, ChannelsAvailabilityError, Messages } from "./types";
|
||||
import { getChannelsAvailabilityMessages } from "./utils";
|
||||
|
||||
|
@ -36,7 +37,9 @@ export type ChannelsAvailabilityCardProps = RequireOnlyOne<
|
|||
"channels" | "channelsList"
|
||||
>;
|
||||
|
||||
export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = props => {
|
||||
export const ChannelsAvailability: React.FC<
|
||||
ChannelsAvailabilityCardProps
|
||||
> = props => {
|
||||
const {
|
||||
channelsList,
|
||||
errors = [],
|
||||
|
@ -49,7 +52,6 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = pro
|
|||
} = props;
|
||||
const intl = useIntl();
|
||||
const localizeDate = useDateLocalize();
|
||||
const classes = useStyles({});
|
||||
|
||||
const channelsMessages = getChannelsAvailabilityMessages({
|
||||
messages,
|
||||
|
@ -88,12 +90,10 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityCardProps> = pro
|
|||
: channelsList
|
||||
? channelsList.map(data => (
|
||||
<React.Fragment key={data.id}>
|
||||
<div className={classes.channelItem}>
|
||||
<div className={classes.channelName}>
|
||||
<Typography>{data.name}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Hr className={classes.hr} />
|
||||
<Box>
|
||||
<Text>{data.name}</Text>
|
||||
</Box>
|
||||
<Divider />
|
||||
</React.Fragment>
|
||||
))
|
||||
: null}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import RequirePermissions from "@dashboard/components/RequirePermissions";
|
||||
import { PermissionEnum } from "@dashboard/graphql";
|
||||
import { Card, CardContent, Typography } from "@material-ui/core";
|
||||
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { useStyles } from "./styles";
|
||||
import { DashboardCard } from "../Card";
|
||||
|
||||
export interface ChannelsAvailabilityWrapperProps {
|
||||
selectedChannelsCount: number;
|
||||
|
@ -17,7 +14,9 @@ export interface ChannelsAvailabilityWrapperProps {
|
|||
openModal: () => void;
|
||||
}
|
||||
|
||||
export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperProps> = props => {
|
||||
export const ChannelsAvailabilityCardWrapper: React.FC<
|
||||
ChannelsAvailabilityWrapperProps
|
||||
> = props => {
|
||||
const {
|
||||
selectedChannelsCount,
|
||||
allChannelsCount,
|
||||
|
@ -26,12 +25,11 @@ export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperPr
|
|||
openModal,
|
||||
} = props;
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
const channelsAvailabilityText = intl.formatMessage(
|
||||
{
|
||||
id: "vY2lpx",
|
||||
id: "AD1PlC",
|
||||
defaultMessage:
|
||||
"Available at {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}",
|
||||
"In {selectedChannelsCount} out of {allChannelsCount, plural, one {# channel} other {# channels}}",
|
||||
description: "channels availability text",
|
||||
},
|
||||
{
|
||||
|
@ -41,43 +39,42 @@ export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperPr
|
|||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
id: "5A6/2C",
|
||||
defaultMessage: "Availability",
|
||||
description: "section header",
|
||||
})}
|
||||
toolbar={
|
||||
<RequirePermissions requiredPermissions={managePermissions}>
|
||||
<Button
|
||||
onClick={openModal}
|
||||
data-test-id="channels-availability-manage-button"
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "2i81/P",
|
||||
defaultMessage: "Manage",
|
||||
description: "section header button",
|
||||
})}
|
||||
</Button>
|
||||
</RequirePermissions>
|
||||
}
|
||||
/>
|
||||
<CardContent className={classes.card}>
|
||||
{!!channelsAvailabilityText && (
|
||||
<>
|
||||
<Typography className={classes.channelInfo}>
|
||||
{channelsAvailabilityText}
|
||||
</Typography>
|
||||
<Hr className={classes.hr} />
|
||||
</>
|
||||
)}
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Box display={"flex"} flexDirection={"column"} gap={3}>
|
||||
<div>
|
||||
{intl.formatMessage({
|
||||
id: "5A6/2C",
|
||||
defaultMessage: "Availability",
|
||||
description: "section header",
|
||||
})}
|
||||
</div>
|
||||
{!!channelsAvailabilityText && (
|
||||
<Text variant={"caption"}>{channelsAvailabilityText}</Text>
|
||||
)}
|
||||
</Box>
|
||||
<RequirePermissions requiredPermissions={managePermissions}>
|
||||
<Button
|
||||
onClick={openModal}
|
||||
data-test-id="channels-availability-manage-button"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "2i81/P",
|
||||
defaultMessage: "Manage",
|
||||
description: "section header button",
|
||||
})}
|
||||
</Button>
|
||||
</RequirePermissions>
|
||||
</Box>
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content gap={3} display="flex" flexDirection="column">
|
||||
<Box display="flex" flexDirection="column" gap={8}>
|
||||
{children}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
</Box>
|
||||
</DashboardCard.Content>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelsAvailabilityWrapper;
|
||||
|
|
|
@ -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" },
|
||||
);
|
|
@ -7,11 +7,10 @@ import { commonMessages } from "@dashboard/intl";
|
|||
import { joinDateTime, splitDateTime } from "@dashboard/misc";
|
||||
import { TextField } from "@material-ui/core";
|
||||
import { TextFieldProps } from "@material-ui/core/TextField";
|
||||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
type DateTimeFieldProps = Omit<TextFieldProps, "label" | "error"> & {
|
||||
onChange: (value: string) => void;
|
||||
error: ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment;
|
||||
|
@ -26,12 +25,11 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
|
|||
value,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles();
|
||||
|
||||
const parsedValue = value ? splitDateTime(value) : { date: "", time: "" };
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" gap={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
|
@ -47,11 +45,6 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
|
|||
type="date"
|
||||
value={parsedValue.date}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
classes: {
|
||||
root: classes.dateField,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
|
@ -68,12 +61,7 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
|
|||
type="time"
|
||||
value={parsedValue.time}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
classes: {
|
||||
root: classes.timeField,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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" },
|
||||
);
|
|
@ -1,8 +1,6 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import { FileFragment } from "@dashboard/graphql";
|
||||
import { commonMessages } from "@dashboard/intl";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import { DeleteIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
|
||||
import { Box, Button, Text, TrashBinIcon } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
|
@ -19,7 +17,6 @@ export interface FileUploadFieldProps {
|
|||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
>;
|
||||
className?: string;
|
||||
disabled: boolean;
|
||||
loading: boolean;
|
||||
file: FileChoiceType;
|
||||
|
@ -29,44 +26,17 @@ export interface FileUploadFieldProps {
|
|||
onFileDelete: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
errorText: {
|
||||
color: theme.palette.error.light,
|
||||
},
|
||||
fileField: {
|
||||
display: "none",
|
||||
},
|
||||
fileUrl: {
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: "none",
|
||||
},
|
||||
uploadFileContent: {
|
||||
alignItems: "center",
|
||||
color: theme.palette.primary.main,
|
||||
display: "flex",
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
},
|
||||
uploadFileName: {
|
||||
minWidth: "6rem",
|
||||
},
|
||||
}),
|
||||
{ name: "FileUploadField" },
|
||||
);
|
||||
|
||||
const FileUploadField: React.FC<FileUploadFieldProps> = props => {
|
||||
const {
|
||||
loading,
|
||||
disabled,
|
||||
file,
|
||||
className,
|
||||
error,
|
||||
helperText,
|
||||
onFileUpload,
|
||||
onFileDelete,
|
||||
inputProps,
|
||||
} = props;
|
||||
const classes = useStyles({});
|
||||
const intl = useIntl();
|
||||
|
||||
const fileInputAnchor = React.createRef<HTMLInputElement>();
|
||||
|
@ -85,52 +55,46 @@ const FileUploadField: React.FC<FileUploadFieldProps> = props => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={className}>
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
{file.label ? (
|
||||
<div className={classes.uploadFileContent}>
|
||||
<div className={classes.uploadFileName}>
|
||||
<Box display="flex" gap={5} alignItems="center">
|
||||
<Text variant="caption">
|
||||
{loading ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<a
|
||||
href={file.file?.url}
|
||||
target="blank"
|
||||
className={classes.fileUrl}
|
||||
>
|
||||
<a href={file.file?.url} target="blank">
|
||||
{file.label}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<IconButton
|
||||
</Text>
|
||||
<Button
|
||||
icon={<TrashBinIcon />}
|
||||
variant="secondary"
|
||||
color="primary"
|
||||
onClick={handleFileDelete}
|
||||
disabled={disabled || loading}
|
||||
data-test-id="button-delete-file"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
type="button"
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<div>
|
||||
<Button
|
||||
onClick={clickFileInput}
|
||||
disabled={disabled || loading}
|
||||
variant="secondary"
|
||||
data-test-id="button-upload-file"
|
||||
>
|
||||
{intl.formatMessage(commonMessages.chooseFile)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={clickFileInput}
|
||||
disabled={disabled || loading}
|
||||
variant="secondary"
|
||||
data-test-id="button-upload-file"
|
||||
type="button"
|
||||
>
|
||||
{intl.formatMessage(commonMessages.chooseFile)}
|
||||
</Button>
|
||||
)}
|
||||
{error && (
|
||||
<Typography variant="caption" className={classes.errorText}>
|
||||
<Text variant="caption" color="textCriticalDefault">
|
||||
{helperText}
|
||||
</Typography>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
<input
|
||||
className={classes.fileField}
|
||||
style={{ display: "none" }}
|
||||
id="fileUpload"
|
||||
onChange={event => onFileUpload(event.target.files[0])}
|
||||
type="file"
|
||||
|
|
|
@ -1,32 +1,21 @@
|
|||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { vars } from "@saleor/macaw-ui/next";
|
||||
import clsx from "clsx";
|
||||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
interface HrProps {
|
||||
/**
|
||||
* @deprecated use `Divider` component from `@saleor/macaw-ui/next`
|
||||
*/
|
||||
export const Hr: React.FC<{
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
root: {
|
||||
backgroundColor: vars.colors.border.neutralPlain,
|
||||
border: "none",
|
||||
display: "block",
|
||||
height: 1,
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
{ name: "Hr" },
|
||||
}> = ({ className }) => (
|
||||
<Box
|
||||
as="hr"
|
||||
className={className}
|
||||
backgroundColor="surfaceNeutralDepressed"
|
||||
borderWidth={0}
|
||||
width="100%"
|
||||
height={1}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Hr: React.FC<HrProps> = props => {
|
||||
const { className } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
|
||||
return <hr className={clsx(classes.root, className)} />;
|
||||
};
|
||||
Hr.displayName = "Hr";
|
||||
export default Hr;
|
||||
|
|
|
@ -5,7 +5,7 @@ import userEvent from "@testing-library/user-event";
|
|||
import React from "react";
|
||||
|
||||
import { props } from "./fixtures";
|
||||
import Metadata from "./Metadata";
|
||||
import { Metadata } from "./Metadata";
|
||||
|
||||
const Component = () => {
|
||||
const { change, data } = useForm(props.data, jest.fn());
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { MetadataInput } from "@dashboard/graphql";
|
||||
import { ChangeEvent } from "@dashboard/hooks/useForm";
|
||||
import { removeAtIndex, updateAtIndex } from "@dashboard/utils/lists";
|
||||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
|
||||
import CardSpacer from "../CardSpacer";
|
||||
import MetadataCard, { MetadataCardProps } from "./MetadataCard";
|
||||
import { MetadataCard, MetadataCardProps } from "./MetadataCard";
|
||||
import { EventDataAction, EventDataField } from "./types";
|
||||
import { getDataKey, parseEventData } from "./utils";
|
||||
|
||||
|
@ -13,7 +13,7 @@ export interface MetadataProps
|
|||
data: Record<"metadata" | "privateMetadata", MetadataInput[]>;
|
||||
}
|
||||
|
||||
const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
|
||||
export const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
|
||||
const change = (event: ChangeEvent, isPrivate: boolean) => {
|
||||
const { action, field, fieldIndex, value } = parseEventData(event);
|
||||
const key = getDataKey(isPrivate);
|
||||
|
@ -53,21 +53,17 @@ const Metadata: React.FC<MetadataProps> = ({ data, onChange }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="grid" gap={5} paddingBottom={9}>
|
||||
<MetadataCard
|
||||
data={data?.metadata}
|
||||
isPrivate={false}
|
||||
onChange={event => change(event, false)}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<MetadataCard
|
||||
data={data?.privateMetadata}
|
||||
isPrivate={true}
|
||||
onChange={event => change(event, true)}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Metadata.displayName = "Metadata";
|
||||
export default Metadata;
|
||||
|
|
|
@ -1,27 +1,44 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
||||
import { MetadataInput } from "@dashboard/graphql";
|
||||
import { FormChange } from "@dashboard/hooks/useForm";
|
||||
import {
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { DeleteIcon, ExpandIcon, IconButton } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ChervonDownIcon,
|
||||
ChervonUpIcon,
|
||||
Text,
|
||||
TrashBinIcon,
|
||||
vars,
|
||||
} from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import CardTitle from "../CardTitle";
|
||||
import { DashboardCard } from "../Card";
|
||||
import Skeleton from "../Skeleton";
|
||||
import useStyles from "./styles";
|
||||
import { EventDataAction, EventDataField } from "./types";
|
||||
import { EventDataAction } from "./types";
|
||||
import { nameInputPrefix, nameSeparator, valueInputPrefix } from "./utils";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
input: {
|
||||
padding: theme.spacing(0.5, 2),
|
||||
},
|
||||
nameInput: {
|
||||
padding: "13px 16px",
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "Metadata",
|
||||
},
|
||||
);
|
||||
|
||||
export interface MetadataCardProps {
|
||||
data: MetadataInput[];
|
||||
|
@ -29,11 +46,7 @@ export interface MetadataCardProps {
|
|||
onChange: FormChange;
|
||||
}
|
||||
|
||||
export const nameSeparator = ":";
|
||||
export const nameInputPrefix = EventDataField.name;
|
||||
export const valueInputPrefix = EventDataField.value;
|
||||
|
||||
const MetadataCard: React.FC<MetadataCardProps> = ({
|
||||
export const MetadataCard: React.FC<MetadataCardProps> = ({
|
||||
data,
|
||||
isPrivate,
|
||||
onChange,
|
||||
|
@ -43,49 +56,47 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
|
|||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Card
|
||||
<DashboardCard
|
||||
data-test-id="metadata-editor"
|
||||
data-test-is-private={isPrivate}
|
||||
data-test-expanded={expanded}
|
||||
gap={3}
|
||||
>
|
||||
<CardTitle
|
||||
className={classes.header}
|
||||
title={
|
||||
<>
|
||||
{isPrivate
|
||||
? intl.formatMessage({
|
||||
id: "ETHnjq",
|
||||
defaultMessage: "Private Metadata",
|
||||
description: "header",
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: "VcI+Zh",
|
||||
defaultMessage: "Metadata",
|
||||
description: "header",
|
||||
})}
|
||||
<IconButton
|
||||
className={clsx(classes.expandBtn, {
|
||||
[classes.rotate]: expanded,
|
||||
<DashboardCard.Title>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
cursor="pointer"
|
||||
>
|
||||
{isPrivate
|
||||
? intl.formatMessage({
|
||||
id: "ETHnjq",
|
||||
defaultMessage: "Private Metadata",
|
||||
description: "header",
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: "VcI+Zh",
|
||||
defaultMessage: "Metadata",
|
||||
description: "header",
|
||||
})}
|
||||
hoverOutline={false}
|
||||
variant="secondary"
|
||||
data-test-id="expand"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<ExpandIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-test-id="expand"
|
||||
type="button"
|
||||
icon={expanded ? <ChervonUpIcon /> : <ChervonDownIcon />}
|
||||
/>
|
||||
</Box>
|
||||
</DashboardCard.Title>
|
||||
{data === undefined ? (
|
||||
<CardContent>
|
||||
<DashboardCard.Content>
|
||||
<Skeleton />
|
||||
</CardContent>
|
||||
</DashboardCard.Content>
|
||||
) : (
|
||||
<>
|
||||
<CardContent className={classes.content}>
|
||||
{data.length > 0 && (
|
||||
<Typography color="textSecondary" variant="body2">
|
||||
{data.length > 0 && (
|
||||
<DashboardCard.Content>
|
||||
<Text variant="caption">
|
||||
<FormattedMessage
|
||||
id="2+v1wX"
|
||||
defaultMessage="{number,plural,one{{number} string} other{{number} strings}}"
|
||||
|
@ -94,52 +105,63 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
|
|||
number: data.length,
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Text>
|
||||
</DashboardCard.Content>
|
||||
)}
|
||||
{expanded && (
|
||||
<>
|
||||
{data.length === 0 ? (
|
||||
<CardContent className={classes.emptyContainer}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<DashboardCard.Content>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage
|
||||
id="cY6H2C"
|
||||
defaultMessage="No metadata created for this element. Use the button below to add new metadata field."
|
||||
description="empty metadata text"
|
||||
/>
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Text>
|
||||
</DashboardCard.Content>
|
||||
) : (
|
||||
<Table className={classes.table}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRowLink>
|
||||
<TableCell className={classes.colNameHeader}>
|
||||
<FormattedMessage
|
||||
id="nudPsY"
|
||||
defaultMessage="Field"
|
||||
description="metadata field name, header"
|
||||
/>
|
||||
<TableCell style={{ paddingLeft: vars.space[9] }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage
|
||||
id="nudPsY"
|
||||
defaultMessage="Field"
|
||||
description="metadata field name, header"
|
||||
/>
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colValue}>
|
||||
<FormattedMessage
|
||||
id="LkuDEb"
|
||||
defaultMessage="Value"
|
||||
description="metadata field value, header"
|
||||
/>
|
||||
<TableCell style={{ paddingLeft: vars.space[11] }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage
|
||||
id="LkuDEb"
|
||||
defaultMessage="Value"
|
||||
description="metadata field value, header"
|
||||
/>
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colActionHeader}>
|
||||
<FormattedMessage
|
||||
id="nEixpu"
|
||||
defaultMessage="Actions"
|
||||
description="table action"
|
||||
/>
|
||||
<TableCell
|
||||
style={{
|
||||
textAlign: "end",
|
||||
paddingRight: vars.space[9],
|
||||
}}
|
||||
>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage
|
||||
id="nEixpu"
|
||||
defaultMessage="Actions"
|
||||
description="table action"
|
||||
/>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((field, fieldIndex) => (
|
||||
<TableRowLink data-test-id="field" key={fieldIndex}>
|
||||
<TableCell className={classes.colName}>
|
||||
<TableCell style={{ paddingLeft: vars.space[9] }}>
|
||||
<TextField
|
||||
InputProps={{
|
||||
classes: {
|
||||
|
@ -155,7 +177,7 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
|
|||
value={field.key}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colValue}>
|
||||
<TableCell>
|
||||
<TextField
|
||||
InputProps={{
|
||||
classes: {
|
||||
|
@ -172,31 +194,34 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
|
|||
value={field.value}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colAction}>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
data-test-id={"delete-field-" + fieldIndex}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
target: {
|
||||
name: EventDataAction.delete,
|
||||
value: fieldIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<TableCell style={{ paddingRight: vars.space[9] }}>
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-test-id={"delete-field-" + fieldIndex}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
target: {
|
||||
name: EventDataAction.delete,
|
||||
value: fieldIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
icon={<TrashBinIcon />}
|
||||
/>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<CardActions className={classes.actions}>
|
||||
<DashboardCard.Content marginTop={5} paddingLeft={9}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-test-id="add-field"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange({
|
||||
target: {
|
||||
|
@ -212,14 +237,11 @@ const MetadataCard: React.FC<MetadataCardProps> = ({
|
|||
description="add metadata field,button"
|
||||
/>
|
||||
</Button>
|
||||
</CardActions>
|
||||
</DashboardCard.Content>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
MetadataCard.displayName = "MetadataCard";
|
||||
export default MetadataCard;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
export * from "./Metadata";
|
||||
export * from "./MetadataCard";
|
||||
export * from "./types";
|
||||
export { default } from "./Metadata";
|
||||
export { default as MetadataCard } from "./MetadataCard";
|
||||
|
|
|
@ -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;
|
|
@ -1,8 +1,11 @@
|
|||
import { ChangeEvent } from "@dashboard/hooks/useForm";
|
||||
|
||||
import { nameSeparator } from "./MetadataCard";
|
||||
import { EventData, EventDataAction, EventDataField } from "./types";
|
||||
|
||||
export const nameSeparator = ":";
|
||||
export const nameInputPrefix = EventDataField.name;
|
||||
export const valueInputPrefix = EventDataField.value;
|
||||
|
||||
export function parseEventData(event: ChangeEvent): EventData {
|
||||
let action: EventDataAction;
|
||||
let field: EventDataField = null;
|
||||
|
|
|
@ -38,11 +38,14 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|||
helperText,
|
||||
editorRef,
|
||||
onInitialize,
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
const id = useId(defaultId);
|
||||
const [isFocused, setIsFocused] = React.useState(false);
|
||||
const [hasValue, setHasValue] = React.useState(false);
|
||||
const isTyped = Boolean(hasValue || isFocused);
|
||||
|
||||
const handleInitialize = React.useCallback((editor: EditorCore) => {
|
||||
if (onInitialize) {
|
||||
|
@ -68,7 +71,17 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|||
fullWidth
|
||||
variant="outlined"
|
||||
>
|
||||
<InputLabel focused={true} shrink={true}>
|
||||
<InputLabel
|
||||
focused={true}
|
||||
shrink={true}
|
||||
classes={{
|
||||
disabled: classes.labelDisabled,
|
||||
error: classes.labelError,
|
||||
root: classes.labelRoot,
|
||||
}}
|
||||
error={error}
|
||||
disabled={disabled}
|
||||
>
|
||||
{label}
|
||||
</InputLabel>
|
||||
{hasRendered && (
|
||||
|
@ -76,17 +89,25 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|||
// match with the id of holder div
|
||||
holder={id}
|
||||
tools={tools}
|
||||
// LogLeves is undefined at runtime
|
||||
// Log level is undefined at runtime
|
||||
logLevel={"ERROR" as LogLevels.ERROR}
|
||||
onInitialize={handleInitialize}
|
||||
onChange={async event => {
|
||||
const editorJsValue = await event.saver.save();
|
||||
setHasValue(editorJsValue.blocks.length > 0);
|
||||
return onChange?.();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
id={id}
|
||||
className={clsx(classes.editor, classes.root, {
|
||||
[classes.rootErrorFocus]: isFocused && error,
|
||||
[classes.rootActive]: isFocused,
|
||||
[classes.rootDisabled]: disabled,
|
||||
[classes.rootError]: error,
|
||||
[classes.rootTyped]:
|
||||
isTyped || props.defaultValue?.blocks?.length > 0,
|
||||
})}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
import { alpha } from "@material-ui/core/styles";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { vars } from "@saleor/macaw-ui/next";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => {
|
||||
const hover = {
|
||||
"&:hover": {
|
||||
background: alpha(theme.palette.primary.main, 0.1),
|
||||
background: vars.colors.background.interactiveNeutralHighlightHovering,
|
||||
},
|
||||
};
|
||||
|
||||
const isDarkMode = theme.palette.type === "dark";
|
||||
|
||||
return {
|
||||
editor: {
|
||||
"& .codex-editor": {
|
||||
|
@ -20,17 +18,18 @@ const useStyles = makeStyles(
|
|||
minHeight: 24,
|
||||
},
|
||||
"& .ce-block--selected .ce-block__content": {
|
||||
background: `${alpha(theme.palette.primary.main, 0.2)} !important`,
|
||||
background: `${vars.colors.background.interactiveNeutralHighlightPressing} !important`,
|
||||
},
|
||||
"& .ce-block__content": {
|
||||
margin: 0,
|
||||
maxWidth: "unset",
|
||||
paddingRight: "54px",
|
||||
},
|
||||
"& .ce-conversion-tool": {
|
||||
...hover,
|
||||
},
|
||||
"& .ce-conversion-tool--focused": {
|
||||
background: `${alpha(theme.palette.primary.main, 0.1)} !important`,
|
||||
background: `${vars.colors.background.interactiveNeutralHighlightHovering} !important`,
|
||||
},
|
||||
"& .ce-conversion-tool__icon": {
|
||||
background: "none",
|
||||
|
@ -77,13 +76,21 @@ const useStyles = makeStyles(
|
|||
...hover,
|
||||
},
|
||||
"& .ce-popover": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
backgroundColor: vars.colors.background.surfaceNeutralPlain,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "-186px",
|
||||
},
|
||||
"& .ce-settings": {
|
||||
position: "absolute",
|
||||
left: "-56px",
|
||||
},
|
||||
"& .ce-popover__item": {
|
||||
...hover,
|
||||
},
|
||||
"& .ce-popover__item-icon": {
|
||||
color: theme.palette.saleor.generic.verydark,
|
||||
color: vars.colors.foreground.iconNeutralDefault,
|
||||
backgroundColor: vars.colors.background.surfaceNeutralPlain,
|
||||
},
|
||||
|
||||
"& .codex-editor__loader": {
|
||||
|
@ -97,39 +104,62 @@ const useStyles = makeStyles(
|
|||
paddingBottom: "0 !important",
|
||||
},
|
||||
"& a": {
|
||||
color: theme.palette.primary.light,
|
||||
color: vars.colors.foreground.textBrandDefault,
|
||||
},
|
||||
"&:not($rootDisabled):hover": {
|
||||
borderColor: isDarkMode
|
||||
? theme.palette.saleor.main[2]
|
||||
: theme.palette.saleor.main[4],
|
||||
"& .ce-popover__item--focused": {
|
||||
background: `${vars.colors.background.interactiveNeutralHighlightHovering} !important`,
|
||||
},
|
||||
"& .cdx-search-field": {
|
||||
backgroundColor: vars.colors.background.surfaceNeutralPlain,
|
||||
},
|
||||
},
|
||||
root: {
|
||||
border: `1px solid ${theme.palette.saleor.main[4]}`,
|
||||
borderRadius: 4,
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
border: "1px solid transparent",
|
||||
borderRadius: vars.borderRadius[3],
|
||||
fontSize: vars.fontSize.bodyMedium,
|
||||
backgroundColor: vars.colors.background.surfaceNeutralHighlight,
|
||||
minHeight: 56,
|
||||
padding: theme.spacing(3, 2),
|
||||
paddingBottom: theme.spacing(),
|
||||
paddingLeft: 10,
|
||||
position: "relative",
|
||||
transition: theme.transitions.duration.short + "ms",
|
||||
padding: theme.spacing(3, 2),
|
||||
paddingBottom: theme.spacing(),
|
||||
paddingLeft: vars.space[4],
|
||||
"&:hover": {
|
||||
border: `1px solid ${vars.colors.border.neutralHighlight}`,
|
||||
},
|
||||
},
|
||||
rootActive: {
|
||||
borderColor: theme.palette.saleor.main[1],
|
||||
border: `1px solid ${vars.colors.border.brandSubdued} !important`,
|
||||
backgroundColor: `${vars.colors.background.interactiveNeutralHighlightDefault} !important`,
|
||||
},
|
||||
rootDisabled: {
|
||||
...theme.overrides.MuiOutlinedInput.root["&$disabled"]["& fieldset"],
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.saleor.main[4],
|
||||
pointerEvents: "none",
|
||||
backgroundColor: vars.colors.background.surfaceNeutralPlain,
|
||||
border: `1px solid ${vars.colors.border.neutralHighlight}`,
|
||||
color: vars.colors.foreground.textNeutralDisabled,
|
||||
},
|
||||
rootError: {
|
||||
borderColor: theme.palette.error.main,
|
||||
backgroundColor: vars.colors.background.surfaceCriticalSubdued,
|
||||
},
|
||||
rootStatic: {
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
},
|
||||
labelRoot: {
|
||||
marginLeft: "-6px",
|
||||
color: `${vars.colors.foreground.textNeutralSubdued} !important`,
|
||||
},
|
||||
labelError: {
|
||||
color: `${vars.colors.foreground.textCriticalSubdued} !important`,
|
||||
},
|
||||
rootErrorFocus: {
|
||||
border: "1px solid transparent !important",
|
||||
},
|
||||
labelDisabled: {
|
||||
color: `${vars.colors.foreground.textNeutralDisabled} !important`,
|
||||
},
|
||||
rootTyped: {
|
||||
backgroundColor: vars.colors.background.surfaceNeutralPlain,
|
||||
},
|
||||
};
|
||||
},
|
||||
{ name: "RichTextEditor" },
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import {
|
||||
CollectionErrorFragment,
|
||||
PageErrorFragment,
|
||||
|
@ -6,15 +5,13 @@ import {
|
|||
} from "@dashboard/graphql";
|
||||
import { getFieldError, getProductErrorMessage } from "@dashboard/utils/errors";
|
||||
import getPageErrorMessage from "@dashboard/utils/errors/page";
|
||||
import { Card, CardContent, TextField, Typography } from "@material-ui/core";
|
||||
import { TextField } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||
import slugify from "slugify";
|
||||
|
||||
import CardTitle from "../CardTitle";
|
||||
import FormSpacer from "../FormSpacer";
|
||||
import { DashboardCard } from "../Card";
|
||||
|
||||
enum SeoField {
|
||||
slug = "slug",
|
||||
|
@ -28,28 +25,7 @@ const maxTitleLength = 70;
|
|||
const maxDescriptionLength = 300;
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
addressBar: {
|
||||
color: "#006621",
|
||||
fontSize: "13px",
|
||||
lineHeight: "16px",
|
||||
marginBottom: "2px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
container: {
|
||||
width: "100%",
|
||||
},
|
||||
descriptionBar: {
|
||||
color: "#545454",
|
||||
fontSize: "13px",
|
||||
lineHeight: "18px",
|
||||
overflowWrap: "break-word",
|
||||
},
|
||||
helperText: {
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
{
|
||||
label: {
|
||||
flex: 1,
|
||||
},
|
||||
|
@ -59,21 +35,7 @@ const useStyles = makeStyles(
|
|||
},
|
||||
display: "flex",
|
||||
},
|
||||
preview: {
|
||||
minHeight: theme.spacing(10),
|
||||
},
|
||||
title: {
|
||||
padding: 0,
|
||||
},
|
||||
titleBar: {
|
||||
color: "#1a0dab",
|
||||
fontSize: "18px",
|
||||
lineHeight: "21px",
|
||||
overflowWrap: "break-word",
|
||||
textDecoration: "none",
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ name: "SeoForm" },
|
||||
);
|
||||
|
||||
|
@ -95,7 +57,7 @@ interface SeoFormProps {
|
|||
onClick?();
|
||||
}
|
||||
|
||||
const SeoForm: React.FC<SeoFormProps> = props => {
|
||||
export const SeoForm: React.FC<SeoFormProps> = props => {
|
||||
const {
|
||||
description,
|
||||
descriptionPlaceholder,
|
||||
|
@ -157,17 +119,18 @@ const SeoForm: React.FC<SeoFormProps> = props => {
|
|||
const getError = (fieldName: SeoField) => getFieldError(errors, fieldName);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
id: "TGX4T1",
|
||||
defaultMessage: "Search Engine Preview",
|
||||
})}
|
||||
toolbar={
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<FormattedMessage
|
||||
defaultMessage="Search Engine Preview"
|
||||
id="TGX4T1"
|
||||
/>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
variant="secondary"
|
||||
onClick={toggleExpansion}
|
||||
data-test-id="edit-seo"
|
||||
type="button"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="s5Imt5"
|
||||
|
@ -175,91 +138,77 @@ const SeoForm: React.FC<SeoFormProps> = props => {
|
|||
description="button"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<CardContent>
|
||||
{shouldDisplayHelperText && (
|
||||
<Typography className={clsx({ [classes.helperText]: expanded })}>
|
||||
{helperText}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content>
|
||||
{shouldDisplayHelperText && <Text>{helperText}</Text>}
|
||||
{expanded && (
|
||||
<div className={classes.container}>
|
||||
<TextField
|
||||
error={!!getError(SeoField.slug) || slug.length > maxSlugLength}
|
||||
name={SeoField.slug}
|
||||
label={
|
||||
<div className={classes.labelContainer}>
|
||||
<div className={classes.label}>
|
||||
<FormattedMessage id="IoDlcd" defaultMessage="Slug" />
|
||||
</div>
|
||||
{slug?.length > 0 && (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="ChAjJu"
|
||||
defaultMessage="{numberOfCharacters} of {maxCharacters} characters"
|
||||
description="character limit"
|
||||
values={{
|
||||
maxCharacters: maxSlugLength,
|
||||
numberOfCharacters: slug?.length,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
InputProps={{
|
||||
inputProps: {
|
||||
maxLength: maxSlugLength,
|
||||
},
|
||||
}}
|
||||
helperText={getSlugHelperMessage()}
|
||||
value={slug}
|
||||
disabled={loading || disabled}
|
||||
placeholder={slug || slugify(slugPlaceholder, { lower: true })}
|
||||
onChange={handleSlugChange}
|
||||
fullWidth
|
||||
/>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
<Box display="grid" gap={5}>
|
||||
<Box>
|
||||
<Input
|
||||
error={!!getError(SeoField.slug) || slug.length > maxSlugLength}
|
||||
name={SeoField.slug}
|
||||
label={
|
||||
<Box display="flex" gap={3}>
|
||||
<Box as="span">
|
||||
<FormattedMessage defaultMessage="Slug" id="IoDlcd" />
|
||||
</Box>
|
||||
{slug?.length > 0 && (
|
||||
<Box as="span">
|
||||
<FormattedMessage
|
||||
defaultMessage="({numberOfCharacters} of {maxCharacters} characters)"
|
||||
id="yi1HSj"
|
||||
values={{
|
||||
maxCharacters: maxSlugLength,
|
||||
numberOfCharacters: slug?.length,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
helperText={getSlugHelperMessage()}
|
||||
size="small"
|
||||
value={slug}
|
||||
onChange={handleSlugChange}
|
||||
disabled={loading || disabled}
|
||||
maxLength={maxSlugLength}
|
||||
placeholder={slugPlaceholder}
|
||||
/>
|
||||
</Box>
|
||||
<Input
|
||||
size="small"
|
||||
error={title?.length > maxTitleLength}
|
||||
name={SeoField.title}
|
||||
label={
|
||||
<div className={classes.labelContainer}>
|
||||
<div className={classes.label}>
|
||||
<FormattedMessage
|
||||
id="w2Cewo"
|
||||
defaultMessage="Search engine title"
|
||||
/>
|
||||
</div>
|
||||
{title?.length > 0 && (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="ChAjJu"
|
||||
defaultMessage="{numberOfCharacters} of {maxCharacters} characters"
|
||||
description="character limit"
|
||||
values={{
|
||||
maxCharacters: maxTitleLength,
|
||||
numberOfCharacters: title.length,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
InputProps={{
|
||||
inputProps: {
|
||||
maxLength: maxTitleLength,
|
||||
},
|
||||
}}
|
||||
helperText={intl.formatMessage(seoFieldMessage)}
|
||||
value={title ?? ""}
|
||||
disabled={loading || disabled}
|
||||
placeholder={titlePlaceholder}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
maxLength={maxTitleLength}
|
||||
placeholder={titlePlaceholder}
|
||||
helperText={intl.formatMessage(seoFieldMessage)}
|
||||
label={
|
||||
<Box display="flex" gap={3}>
|
||||
<Box as="span">
|
||||
<FormattedMessage
|
||||
defaultMessage="Search engine title"
|
||||
id="w2Cewo"
|
||||
/>
|
||||
</Box>
|
||||
{title?.length > 0 && (
|
||||
<Box as="span">
|
||||
<FormattedMessage
|
||||
defaultMessage="({numberOfCharacters} of {maxCharacters} characters)"
|
||||
id="yi1HSj"
|
||||
values={{
|
||||
maxCharacters: maxTitleLength,
|
||||
numberOfCharacters: title?.length,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
error={description?.length > maxDescriptionLength}
|
||||
name={SeoField.description}
|
||||
|
@ -300,11 +249,9 @@ const SeoForm: React.FC<SeoFormProps> = props => {
|
|||
placeholder={descriptionPlaceholder}
|
||||
rows={10}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DashboardCard.Content>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
SeoForm.displayName = "SeoForm";
|
||||
export default SeoForm;
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export { default } from "./SeoForm";
|
||||
export * from "./SeoForm";
|
||||
|
|
|
@ -45,13 +45,15 @@ export interface SingleAutocompleteSelectFieldProps
|
|||
nakedInput?: boolean;
|
||||
onBlur?: () => void;
|
||||
popperPlacement?: PopperPlacementType;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const DebounceAutocomplete: React.ComponentType<DebounceProps<
|
||||
string
|
||||
>> = Debounce;
|
||||
const DebounceAutocomplete: React.ComponentType<DebounceProps<string>> =
|
||||
Debounce;
|
||||
|
||||
const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectFieldProps> = props => {
|
||||
const SingleAutocompleteSelectFieldComponent: React.FC<
|
||||
SingleAutocompleteSelectFieldProps
|
||||
> = props => {
|
||||
const {
|
||||
add,
|
||||
allowCustomValues,
|
||||
|
@ -78,6 +80,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
|
|||
nakedInput = false,
|
||||
onBlur,
|
||||
popperPlacement = "bottom-end",
|
||||
id,
|
||||
...rest
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
|
@ -243,6 +246,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
|
|||
fullWidth={true}
|
||||
onBlur={onBlur}
|
||||
inputRef={input}
|
||||
id={id}
|
||||
/>
|
||||
{isOpen && (!!inputValue || !!choices.length) && (
|
||||
<Popper
|
||||
|
@ -284,11 +288,9 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
|
|||
);
|
||||
};
|
||||
|
||||
const SingleAutocompleteSelectField: React.FC<SingleAutocompleteSelectFieldProps> = ({
|
||||
choices,
|
||||
fetchChoices,
|
||||
...rest
|
||||
}) => {
|
||||
const SingleAutocompleteSelectField: React.FC<
|
||||
SingleAutocompleteSelectFieldProps
|
||||
> = ({ choices, fetchChoices, ...rest }) => {
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
if (fetchChoices) {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * from "./TopNavMenu";
|
|
@ -9,7 +9,7 @@ import CardMenu from "@dashboard/components/CardMenu/CardMenu";
|
|||
import { CardSpacer } from "@dashboard/components/CardSpacer";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
||||
import { MetadataFormData } from "@dashboard/components/Metadata/types";
|
||||
import RequirePermissions from "@dashboard/components/RequirePermissions";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
|
|
|
@ -3,7 +3,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
|||
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import { createSaleChannelsChangeHandler } from "@dashboard/discounts/handlers";
|
||||
import { saleListUrl } from "@dashboard/discounts/urls";
|
||||
|
|
|
@ -4,7 +4,7 @@ import CardSpacer from "@dashboard/components/CardSpacer";
|
|||
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import { Tab, TabContainer } from "@dashboard/components/Tab";
|
||||
import {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
|||
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import {
|
||||
createChannelsChangeHandler,
|
||||
|
|
|
@ -5,7 +5,7 @@ import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailability
|
|||
import CountryList from "@dashboard/components/CountryList";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import { Tab, TabContainer } from "@dashboard/components/Tab";
|
||||
import {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||
import React from "react";
|
||||
|
|
|
@ -9,7 +9,7 @@ import { CardSpacer } from "@dashboard/components/CardSpacer";
|
|||
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import {
|
||||
OrderDetailsFragment,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import HorizontalSpacer from "@dashboard/components/HorizontalSpacer";
|
||||
import { Hr } from "@dashboard/components/Hr";
|
||||
import Money from "@dashboard/components/Money";
|
||||
import { Pill } from "@dashboard/components/Pill";
|
||||
import Skeleton from "@dashboard/components/Skeleton";
|
||||
|
@ -12,6 +11,7 @@ import {
|
|||
OrderStatus,
|
||||
} from "@dashboard/graphql";
|
||||
import { Card, CardContent } from "@material-ui/core";
|
||||
import { Divider } from "@saleor/macaw-ui/next";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
@ -201,7 +201,7 @@ const OrderPayment: React.FC<OrderPaymentProps> = props => {
|
|||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<Hr />
|
||||
<Divider />
|
||||
<CardContent className={classes.payments}>
|
||||
<div className={classes.root}>
|
||||
{!!usedGiftCardAmount && (
|
||||
|
|
|
@ -31,6 +31,7 @@ jest.mock("@saleor/macaw-ui", () => ({
|
|||
|
||||
jest.mock("@saleor/macaw-ui/next", () => ({
|
||||
useTheme: jest.fn(() => () => ({})),
|
||||
Divider: jest.fn(() => <></>),
|
||||
vars: {
|
||||
colors: {
|
||||
border: {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import { Hr } from "@dashboard/components/Hr";
|
||||
import { Pill } from "@dashboard/components/Pill";
|
||||
import Skeleton from "@dashboard/components/Skeleton";
|
||||
import { OrderAction, OrderDetailsFragment } from "@dashboard/graphql";
|
||||
|
@ -10,6 +9,7 @@ import {
|
|||
orderSendRefundUrl,
|
||||
} from "@dashboard/orders/urls";
|
||||
import { Card, CardContent, Typography } from "@material-ui/core";
|
||||
import { Divider } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -94,7 +94,7 @@ const OrderPaymentSummaryCard: React.FC<OrderPaymementProps> = ({
|
|||
<PaymentsSummary order={order} />
|
||||
{canAnyRefund && (
|
||||
<>
|
||||
<Hr />
|
||||
<Divider />
|
||||
<CardTitle
|
||||
toolbar={
|
||||
<div className={classes.refundsButtons}>
|
||||
|
|
|
@ -3,7 +3,7 @@ import Form from "@dashboard/components/Form";
|
|||
import Grid from "@dashboard/components/Grid";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import { PageErrorFragment } from "@dashboard/graphql";
|
||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||
|
|
|
@ -3,7 +3,7 @@ import Form from "@dashboard/components/Form";
|
|||
import Grid from "@dashboard/components/Grid";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata";
|
||||
import { MetadataFormData } from "@dashboard/components/Metadata/types";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";
|
||||
|
|
|
@ -4,12 +4,12 @@ import {
|
|||
} from "@dashboard/attributes/utils/data";
|
||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
|
||||
import Attributes, { AttributeInput } from "@dashboard/components/Attributes";
|
||||
import { AttributeInput, Attributes } from "@dashboard/components/Attributes";
|
||||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import SeoForm from "@dashboard/components/SeoForm";
|
||||
import { SeoForm } from "@dashboard/components/SeoForm";
|
||||
import VisibilityCard from "@dashboard/components/VisibilityCard";
|
||||
import {
|
||||
PageDetailsFragment,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
|||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata, { MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import { Metadata, MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import {
|
||||
ProductTypeKindEnum,
|
||||
|
|
|
@ -3,7 +3,7 @@ import CardSpacer from "@dashboard/components/CardSpacer";
|
|||
import ControlledSwitch from "@dashboard/components/ControlledSwitch";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
||||
import { MetadataFormData } from "@dashboard/components/Metadata/types";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import {
|
||||
|
|
|
@ -6,14 +6,14 @@ import CannotDefineChannelsAvailabilityCard from "@dashboard/channels/components
|
|||
import { ChannelData } from "@dashboard/channels/utils";
|
||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
|
||||
import Attributes, { AttributeInput } from "@dashboard/components/Attributes";
|
||||
import { AttributeInput, Attributes } from "@dashboard/components/Attributes";
|
||||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata";
|
||||
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import SeoForm from "@dashboard/components/SeoForm";
|
||||
import { SeoForm } from "@dashboard/components/SeoForm";
|
||||
import {
|
||||
PermissionEnum,
|
||||
ProductChannelListingErrorFragment,
|
||||
|
@ -30,7 +30,7 @@ import {
|
|||
} from "@dashboard/graphql";
|
||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||
import useStateFromProps from "@dashboard/hooks/useStateFromProps";
|
||||
import ProductVariantPrice from "@dashboard/products/components/ProductVariantPrice";
|
||||
import { ProductVariantPrice } from "@dashboard/products/components/ProductVariantPrice";
|
||||
import {
|
||||
ProductCreateUrlQueryParams,
|
||||
productListUrl,
|
||||
|
@ -41,10 +41,10 @@ import React from "react";
|
|||
import { useIntl } from "react-intl";
|
||||
|
||||
import { FetchMoreProps, RelayToFlat } from "../../../types";
|
||||
import ProductDetailsForm from "../ProductDetailsForm";
|
||||
import ProductOrganization from "../ProductOrganization";
|
||||
import ProductShipping from "../ProductShipping/ProductShipping";
|
||||
import ProductStocks from "../ProductStocks";
|
||||
import { ProductDetailsForm } from "../ProductDetailsForm";
|
||||
import { ProductOrganization } from "../ProductOrganization";
|
||||
import { ProductShipping } from "../ProductShipping";
|
||||
import { ProductStocks } from "../ProductStocks";
|
||||
import ProductTaxes from "../ProductTaxes";
|
||||
import ProductCreateForm, {
|
||||
ProductCreateData,
|
||||
|
@ -263,7 +263,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
|||
richTextGetters={attributeRichTextGetters}
|
||||
/>
|
||||
)}
|
||||
<CardSpacer />
|
||||
{isSimpleProduct && (
|
||||
<>
|
||||
<ProductShipping
|
||||
|
@ -273,14 +272,12 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
|||
weightUnit={weightUnit}
|
||||
onChange={change}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<ProductVariantPrice
|
||||
ProductVariantChannelListings={data.channelListings}
|
||||
errors={channelsErrors}
|
||||
loading={loading}
|
||||
onChange={handlers.changeChannelPrice}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<ProductStocks
|
||||
data={data}
|
||||
disabled={loading}
|
||||
|
@ -315,7 +312,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
|||
loading={loading}
|
||||
onChange={change}
|
||||
/>
|
||||
<CardSpacer />
|
||||
<Metadata data={data} onChange={handlers.changeMetadata} />
|
||||
</DetailPageLayout.Content>
|
||||
<DetailPageLayout.RightSidebar>
|
||||
|
@ -341,7 +337,6 @@ export const ProductCreatePage: React.FC<ProductCreatePageProps> = ({
|
|||
onProductTypeChange={handlers.selectProductType}
|
||||
collectionsInputDisplayValue={selectedCollections}
|
||||
/>
|
||||
<CardSpacer />
|
||||
{isSimpleProduct ? (
|
||||
<ChannelsAvailabilityCard
|
||||
managePermissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import FormSpacer from "@dashboard/components/FormSpacer";
|
||||
import Grid from "@dashboard/components/Grid";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import { DashboardCard } from "@dashboard/components/Card";
|
||||
import RichTextEditor from "@dashboard/components/RichTextEditor";
|
||||
import { RichTextEditorLoading } from "@dashboard/components/RichTextEditor/RichTextEditorLoading";
|
||||
import { ProductErrorFragment } from "@dashboard/graphql";
|
||||
|
@ -9,7 +6,7 @@ import { commonMessages } from "@dashboard/intl";
|
|||
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
|
||||
import { useRichTextContext } from "@dashboard/utils/richText/context";
|
||||
import { OutputData } from "@editorjs/editorjs";
|
||||
import { Card, CardContent, TextField } from "@material-ui/core";
|
||||
import { Box, Input } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
|
@ -27,36 +24,36 @@ interface ProductDetailsFormProps {
|
|||
|
||||
export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
|
||||
data,
|
||||
disabled,
|
||||
errors,
|
||||
onChange,
|
||||
errors,
|
||||
disabled,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const formErrors = getFormErrors(["name", "description", "rating"], errors);
|
||||
const { editorRef, defaultValue, isReadyForMount, handleChange } =
|
||||
useRichTextContext();
|
||||
|
||||
const formErrors = getFormErrors(["name", "description", "rating"], errors);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage(commonMessages.generalInformations)}
|
||||
/>
|
||||
<CardContent>
|
||||
<TextField
|
||||
error={!!formErrors.name}
|
||||
helperText={getProductErrorMessage(formErrors.name, intl)}
|
||||
fullWidth
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
{intl.formatMessage(commonMessages.generalInformations)}
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content display="grid" gap={5} paddingX={8}>
|
||||
<Input
|
||||
label={intl.formatMessage({
|
||||
id: "6AMFki",
|
||||
defaultMessage: "Name",
|
||||
description: "product name",
|
||||
})}
|
||||
name="name"
|
||||
value={data.name}
|
||||
size="small"
|
||||
value={data.name || ""}
|
||||
onChange={onChange}
|
||||
error={!!formErrors.name}
|
||||
name="name"
|
||||
disabled={disabled}
|
||||
helperText={getProductErrorMessage(formErrors.name, intl)}
|
||||
/>
|
||||
<FormSpacer />
|
||||
|
||||
{isReadyForMount ? (
|
||||
<RichTextEditor
|
||||
editorRef={editorRef}
|
||||
|
@ -74,27 +71,24 @@ export const ProductDetailsForm: React.FC<ProductDetailsFormProps> = ({
|
|||
name="description"
|
||||
/>
|
||||
)}
|
||||
<FormSpacer />
|
||||
<Hr />
|
||||
<FormSpacer />
|
||||
<Grid variant="uniform">
|
||||
<TextField
|
||||
type="number"
|
||||
error={!!formErrors.rating}
|
||||
helperText={getProductErrorMessage(formErrors.rating, intl)}
|
||||
disabled={disabled}
|
||||
<Box __width="25%">
|
||||
<Input
|
||||
label={intl.formatMessage({
|
||||
id: "L7N+0y",
|
||||
defaultMessage: "Product Rating",
|
||||
description: "product rating",
|
||||
})}
|
||||
name="rating"
|
||||
size="small"
|
||||
value={data.rating || ""}
|
||||
onChange={onChange}
|
||||
error={!!formErrors.rating}
|
||||
name="rating"
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
helperText={getProductErrorMessage(formErrors.rating, intl)}
|
||||
/>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
</DashboardCard.Content>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
export default ProductDetailsForm;
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export { default } from "./ProductDetailsForm";
|
||||
export * from "./ProductDetailsForm";
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import BackButton from "@dashboard/components/BackButton";
|
||||
import { Button } from "@dashboard/components/Button";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import FormSpacer from "@dashboard/components/FormSpacer";
|
||||
import { ProductFragment } from "@dashboard/graphql";
|
||||
import { SubmitPromise } from "@dashboard/hooks/useForm";
|
||||
import { buttonMessages } from "@dashboard/intl";
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { Button, Input, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -52,33 +50,37 @@ const ProductExternalMediaDialog: React.FC<ProductExternalMediaDialogProps> = ({
|
|||
return (
|
||||
<Dialog onClose={onClose} open={open}>
|
||||
<DialogTitle disableTypography>
|
||||
{intl.formatMessage(messages.buttonMessage)}
|
||||
<Text variant="heading">
|
||||
{intl.formatMessage(messages.buttonMessage)}
|
||||
</Text>
|
||||
</DialogTitle>
|
||||
<Form initial={initialValues} onSubmit={handleOnSubmit}>
|
||||
{({ change, data, submit }) => (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
<Text variant="caption">
|
||||
<FormattedMessage
|
||||
id="zDvDnG"
|
||||
defaultMessage="Media from the URL you supply will be shown in the media gallery. You will be able to define the order of the gallery."
|
||||
description="modal header"
|
||||
/>
|
||||
</Typography>
|
||||
</Text>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
<Input
|
||||
label="URL"
|
||||
value={data.mediaUrl}
|
||||
name="mediaUrl"
|
||||
type="url"
|
||||
type="text"
|
||||
onChange={change}
|
||||
autoFocus
|
||||
fullWidth
|
||||
size="medium"
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<BackButton onClick={onClose} />
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
<FormattedMessage {...buttonMessages.back} />
|
||||
</Button>
|
||||
<Button onClick={submit}>
|
||||
{intl.formatMessage(messages.buttonMessage)}
|
||||
</Button>
|
||||
|
|
|
@ -11,7 +11,6 @@ import { getByName } from "@dashboard/components/Filter/utils";
|
|||
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
|
||||
import { ListPageLayout } from "@dashboard/components/Layouts";
|
||||
import LimitReachedAlert from "@dashboard/components/LimitReachedAlert";
|
||||
import { TopNavMenu } from "@dashboard/components/TopNavMenu";
|
||||
import { ProductListColumns } from "@dashboard/config";
|
||||
import {
|
||||
GridAttributesQuery,
|
||||
|
@ -188,7 +187,7 @@ export const ProductListPage: React.FC<ProductListPageProps> = props => {
|
|||
)}
|
||||
</Text>
|
||||
)}
|
||||
<TopNavMenu
|
||||
<TopNav.Menu
|
||||
dataTestId="menu"
|
||||
items={[
|
||||
{
|
||||
|
|
|
@ -1,121 +1,23 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import { DashboardCard } from "@dashboard/components/Card";
|
||||
import ImageUpload from "@dashboard/components/ImageUpload";
|
||||
import MediaTile from "@dashboard/components/MediaTile";
|
||||
import Skeleton from "@dashboard/components/Skeleton";
|
||||
import { ProductMediaFragment, ProductMediaType } from "@dashboard/graphql";
|
||||
import { ProductMediaPopper } from "@dashboard/products/components/ProductMediaPopper/ProductMediaPopper";
|
||||
import { ReorderAction } from "@dashboard/types";
|
||||
import createMultiFileUploadHandler from "@dashboard/utils/handlers/multiFileUploadHandler";
|
||||
import { Card, CardContent } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { vars } from "@saleor/macaw-ui/next";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dropdown,
|
||||
List,
|
||||
sprinkles,
|
||||
Text,
|
||||
} from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { defineMessages, useIntl } from "react-intl";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { SortableContainer, SortableElement } from "react-sortable-hoc";
|
||||
|
||||
const messages = defineMessages({
|
||||
media: {
|
||||
id: "/Mcvt4",
|
||||
defaultMessage: "Media",
|
||||
description: "section header",
|
||||
},
|
||||
upload: {
|
||||
id: "mGiA6q",
|
||||
defaultMessage: "Upload",
|
||||
description: "modal button upload",
|
||||
},
|
||||
});
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
card: {
|
||||
marginTop: theme.spacing(2),
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
marginTop: 0,
|
||||
},
|
||||
},
|
||||
fileField: {
|
||||
display: "none",
|
||||
},
|
||||
icon: {
|
||||
color: "rgba(255, 255, 255, 0.54)",
|
||||
},
|
||||
image: {
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
userSelect: "none",
|
||||
width: "100%",
|
||||
},
|
||||
imageContainer: {
|
||||
"&:hover, &.dragged": {
|
||||
"& $imageOverlay": {
|
||||
display: "block",
|
||||
},
|
||||
},
|
||||
background: "#ffffff",
|
||||
border: `1px solid ${vars.colors.border.neutralPlain}`,
|
||||
borderRadius: theme.spacing(),
|
||||
height: 140,
|
||||
margin: "auto",
|
||||
overflow: "hidden",
|
||||
padding: theme.spacing(2),
|
||||
position: "relative",
|
||||
width: 140,
|
||||
},
|
||||
imageGridContainer: {
|
||||
position: "relative",
|
||||
},
|
||||
imageOverlay: {
|
||||
background: "rgba(0, 0, 0, 0.6)",
|
||||
cursor: "move",
|
||||
display: "none",
|
||||
height: 140,
|
||||
left: 0,
|
||||
padding: theme.spacing(2),
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
width: 140,
|
||||
},
|
||||
imageOverlayToolbar: {
|
||||
alignContent: "flex-end",
|
||||
display: "flex",
|
||||
position: "relative",
|
||||
right: theme.spacing(-3),
|
||||
top: theme.spacing(-2),
|
||||
},
|
||||
imageUpload: {
|
||||
height: "100%",
|
||||
left: 0,
|
||||
outline: 0,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
width: "100%",
|
||||
},
|
||||
imageUploadActive: {
|
||||
zIndex: 1,
|
||||
},
|
||||
imageUploadIconActive: {
|
||||
display: "block",
|
||||
},
|
||||
root: {
|
||||
display: "grid",
|
||||
gridColumnGap: theme.spacing(2),
|
||||
gridRowGap: theme.spacing(2),
|
||||
gridTemplateColumns: "repeat(4, 1fr)",
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
},
|
||||
[theme.breakpoints.down("xs")]: {
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
},
|
||||
},
|
||||
rootDragActive: {
|
||||
opacity: 0.2,
|
||||
},
|
||||
}),
|
||||
{ name: "ProductMedia" },
|
||||
);
|
||||
import { messages } from "./messages";
|
||||
|
||||
interface SortableMediaProps {
|
||||
media: {
|
||||
|
@ -163,7 +65,6 @@ const MediaListContainer = SortableContainer<MediaListContainerProps>(
|
|||
);
|
||||
|
||||
interface ProductMediaProps {
|
||||
placeholderImage?: string;
|
||||
media: ProductMediaFragment[];
|
||||
loading?: boolean;
|
||||
getImageEditUrl: (id: string) => string;
|
||||
|
@ -176,7 +77,6 @@ interface ProductMediaProps {
|
|||
const ProductMedia: React.FC<ProductMediaProps> = props => {
|
||||
const {
|
||||
media,
|
||||
placeholderImage,
|
||||
getImageEditUrl,
|
||||
onImageDelete,
|
||||
onImageReorder,
|
||||
|
@ -184,14 +84,12 @@ const ProductMedia: React.FC<ProductMediaProps> = props => {
|
|||
openMediaUrlModal,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const imagesUpload = React.useRef<HTMLInputElement>(null);
|
||||
const anchor = React.useRef<HTMLButtonElement>();
|
||||
const [imagesToUpload, setImagesToUpload] = React.useState<
|
||||
ProductMediaFragment[]
|
||||
>([]);
|
||||
const [popperOpenStatus, setPopperOpenStatus] = React.useState(false);
|
||||
|
||||
const handleImageUpload = createMultiFileUploadHandler(onImageUpload, {
|
||||
onAfterUpload: () =>
|
||||
|
@ -219,61 +117,92 @@ const ProductMedia: React.FC<ProductMediaProps> = props => {
|
|||
});
|
||||
|
||||
return (
|
||||
<Card className={classes.card}>
|
||||
<CardTitle
|
||||
title={intl.formatMessage(messages.media)}
|
||||
toolbar={
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setPopperOpenStatus(true)}
|
||||
variant="tertiary"
|
||||
data-test-id="button-upload-image"
|
||||
ref={anchor}
|
||||
>
|
||||
{intl.formatMessage(messages.upload)}
|
||||
</Button>
|
||||
|
||||
<ProductMediaPopper
|
||||
anchorRef={anchor.current}
|
||||
imagesUploadRef={imagesUpload.current}
|
||||
setPopperStatus={setPopperOpenStatus}
|
||||
popperStatus={popperOpenStatus}
|
||||
openMediaUrlModal={openMediaUrlModal}
|
||||
/>
|
||||
|
||||
<input
|
||||
className={classes.fileField}
|
||||
id="fileUpload"
|
||||
onChange={event => handleImageUpload(event.target.files)}
|
||||
multiple
|
||||
type="file"
|
||||
ref={imagesUpload}
|
||||
accept="image/*"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className={classes.imageGridContainer}>
|
||||
{media === undefined ? (
|
||||
<CardContent>
|
||||
<div className={classes.root}>
|
||||
<div className={classes.imageContainer}>
|
||||
<img className={classes.image} src={placeholderImage} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
) : media.length > 0 ? (
|
||||
<>
|
||||
<ImageUpload
|
||||
className={classes.imageUpload}
|
||||
isActiveClassName={classes.imageUploadActive}
|
||||
disableClick={true}
|
||||
hideUploadIcon={true}
|
||||
iconContainerActiveClassName={classes.imageUploadIconActive}
|
||||
onImageUpload={handleImageUpload}
|
||||
>
|
||||
{({ isDragActive }) => (
|
||||
<CardContent>
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
<Box display="flex" justifyContent="space-between" cursor="pointer">
|
||||
<FormattedMessage {...messages.media} />
|
||||
<Dropdown>
|
||||
<Dropdown.Trigger>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
data-test-id="button-upload-image"
|
||||
ref={anchor}
|
||||
>
|
||||
{intl.formatMessage(messages.upload)}
|
||||
</Button>
|
||||
</Dropdown.Trigger>
|
||||
<Dropdown.Content align="end">
|
||||
<List
|
||||
padding={5}
|
||||
borderRadius={4}
|
||||
boxShadow="overlay"
|
||||
backgroundColor="surfaceNeutralPlain"
|
||||
>
|
||||
<Dropdown.Item>
|
||||
<List.Item
|
||||
borderRadius={4}
|
||||
paddingX={4}
|
||||
paddingY={5}
|
||||
onClick={() => imagesUpload.current.click()}
|
||||
data-test-id="upload-images"
|
||||
>
|
||||
<Text>{intl.formatMessage(messages.uploadImages)}</Text>
|
||||
</List.Item>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item>
|
||||
<List.Item
|
||||
borderRadius={4}
|
||||
paddingX={4}
|
||||
paddingY={5}
|
||||
onClick={openMediaUrlModal}
|
||||
data-test-id="upload-media-url"
|
||||
>
|
||||
<Text>{intl.formatMessage(messages.uploadUrl)}</Text>
|
||||
</List.Item>
|
||||
</Dropdown.Item>
|
||||
</List>
|
||||
</Dropdown.Content>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content>
|
||||
<Box>
|
||||
<Box
|
||||
as="input"
|
||||
display="none"
|
||||
id="fileUpload"
|
||||
onChange={(event: 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
|
||||
distance={20}
|
||||
helperClass="dragged"
|
||||
|
@ -281,22 +210,24 @@ const ProductMedia: React.FC<ProductMediaProps> = props => {
|
|||
media={media}
|
||||
preview={imagesToUpload}
|
||||
onSortEnd={onImageReorder}
|
||||
className={clsx({
|
||||
[classes.root]: true,
|
||||
[classes.rootDragActive]: isDragActive,
|
||||
className={sprinkles({
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
gridTemplateColumns: { mobile: 2, tablet: 3, desktop: 4 },
|
||||
opacity: isDragActive ? "0.2" : "1",
|
||||
})}
|
||||
onDelete={onImageDelete}
|
||||
getEditHref={getImageEditUrl}
|
||||
/>
|
||||
</CardContent>
|
||||
)}
|
||||
</ImageUpload>
|
||||
</>
|
||||
) : (
|
||||
<ImageUpload onImageUpload={handleImageUpload} />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</ImageUpload>
|
||||
</>
|
||||
) : (
|
||||
<ImageUpload onImageUpload={handleImageUpload} />
|
||||
)}
|
||||
</Box>
|
||||
</DashboardCard.Content>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
ProductMedia.displayName = "ProductMedia";
|
||||
|
|
24
src/products/components/ProductMedia/messages.ts
Normal file
24
src/products/components/ProductMedia/messages.ts
Normal 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",
|
||||
},
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -1,7 +1,4 @@
|
|||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import { FormSpacer } from "@dashboard/components/FormSpacer";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import { DashboardCard } from "@dashboard/components/Card";
|
||||
import Link from "@dashboard/components/Link";
|
||||
import MultiAutocompleteSelectField, {
|
||||
MultiAutocompleteChoiceType,
|
||||
|
@ -16,12 +13,10 @@ import {
|
|||
} from "@dashboard/graphql";
|
||||
import { ChangeEvent } from "@dashboard/hooks/useForm";
|
||||
import { commonMessages } from "@dashboard/intl";
|
||||
import { maybe } from "@dashboard/misc";
|
||||
import { productTypeUrl } from "@dashboard/productTypes/urls";
|
||||
import { FetchMoreProps } from "@dashboard/types";
|
||||
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
|
||||
import { Card, CardContent, Typography } from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -31,22 +26,6 @@ interface ProductType {
|
|||
name: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
card: {
|
||||
overflow: "visible",
|
||||
},
|
||||
cardSubtitle: {
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
marginBottom: theme.spacing(0.5),
|
||||
},
|
||||
label: {
|
||||
marginBottom: theme.spacing(0.5),
|
||||
},
|
||||
}),
|
||||
{ name: "ProductOrganization" },
|
||||
);
|
||||
|
||||
interface ProductOrganizationProps {
|
||||
canChangeType: boolean;
|
||||
categories?: SingleAutocompleteChoiceType[];
|
||||
|
@ -74,7 +53,9 @@ interface ProductOrganizationProps {
|
|||
onProductTypeChange?: (event: ChangeEvent) => void;
|
||||
}
|
||||
|
||||
const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
|
||||
export const ProductOrganization: React.FC<
|
||||
ProductOrganizationProps
|
||||
> = props => {
|
||||
const {
|
||||
canChangeType,
|
||||
categories,
|
||||
|
@ -98,7 +79,6 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
|
|||
onProductTypeChange,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
const formErrors = getFormErrors(
|
||||
|
@ -111,15 +91,15 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
|
|||
: null;
|
||||
|
||||
return (
|
||||
<Card className={classes.card}>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
{intl.formatMessage({
|
||||
id: "JjeZEG",
|
||||
defaultMessage: "Organize Product",
|
||||
description: "section header",
|
||||
})}
|
||||
/>
|
||||
<CardContent>
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content gap={8} display="flex" flexDirection="column">
|
||||
{canChangeType ? (
|
||||
<SingleAutocompleteSelectField
|
||||
displayValue={productTypeInputDisplayValue}
|
||||
|
@ -139,36 +119,33 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
|
|||
{...fetchMoreProductTypes}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Typography className={classes.label} variant="caption">
|
||||
<FormattedMessage id="anK7jD" defaultMessage="Product Type" />
|
||||
</Typography>
|
||||
<Typography>
|
||||
<Link
|
||||
href={productTypeUrl(productType?.id) ?? ""}
|
||||
disabled={!productType?.id}
|
||||
>
|
||||
{productType?.name ?? "..."}
|
||||
</Link>
|
||||
</Typography>
|
||||
<CardSpacer />
|
||||
<Typography className={classes.label} variant="caption">
|
||||
<FormattedMessage id="Be+J13" defaultMessage="Configurable" />
|
||||
</Typography>
|
||||
<Typography>
|
||||
{maybe(
|
||||
() =>
|
||||
productType.hasVariants
|
||||
? intl.formatMessage(commonMessages.yes)
|
||||
: intl.formatMessage(commonMessages.no),
|
||||
"...",
|
||||
)}
|
||||
</Typography>
|
||||
</>
|
||||
<Box display="flex" flexDirection="column" gap={6}>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Text variant="bodyEmp">
|
||||
<FormattedMessage id="anK7jD" defaultMessage="Product Type" />
|
||||
</Text>
|
||||
<Text variant="caption">
|
||||
<Link
|
||||
href={productTypeUrl(productType?.id) ?? ""}
|
||||
disabled={!productType?.id}
|
||||
>
|
||||
{productType?.name ?? "..."}
|
||||
</Link>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Text variant="bodyEmp">
|
||||
<FormattedMessage id="Be+J13" defaultMessage="Configurable" />
|
||||
</Text>
|
||||
<Text variant="caption">
|
||||
{productType?.hasVariants
|
||||
? intl.formatMessage(commonMessages.yes)
|
||||
: intl.formatMessage(commonMessages.no)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<FormSpacer />
|
||||
<Hr />
|
||||
<FormSpacer />
|
||||
<SingleAutocompleteSelectField
|
||||
displayValue={categoryInputDisplayValue}
|
||||
error={!!(formErrors.category || noCategoryError)}
|
||||
|
@ -189,9 +166,6 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
|
|||
data-test-id="category"
|
||||
{...fetchMoreCategories}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<Hr />
|
||||
<FormSpacer />
|
||||
<MultiAutocompleteSelectField
|
||||
displayValues={collectionsInputDisplayValue}
|
||||
error={!!formErrors.collections}
|
||||
|
@ -217,9 +191,7 @@ const ProductOrganization: React.FC<ProductOrganizationProps> = props => {
|
|||
testId="collection"
|
||||
{...fetchMoreCollections}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DashboardCard.Content>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
ProductOrganization.displayName = "ProductOrganization";
|
||||
export default ProductOrganization;
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export { default } from "./ProductOrganization";
|
||||
export * from "./ProductOrganization";
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import Grid from "@dashboard/components/Grid";
|
||||
import { DashboardCard } from "@dashboard/components/Card";
|
||||
import { ProductErrorFragment } from "@dashboard/graphql";
|
||||
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
|
||||
import createNonNegativeValueChangeHandler from "@dashboard/utils/handlers/nonNegativeValueChangeHandler";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
} from "@material-ui/core";
|
||||
import { InputAdornment, TextField } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
|
@ -22,7 +16,7 @@ interface ProductShippingProps {
|
|||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
}
|
||||
|
||||
const ProductShipping: React.FC<ProductShippingProps> = props => {
|
||||
export const ProductShipping: React.FC<ProductShippingProps> = props => {
|
||||
const { data, disabled, errors, weightUnit, onChange } = props;
|
||||
|
||||
const intl = useIntl();
|
||||
|
@ -31,43 +25,38 @@ const ProductShipping: React.FC<ProductShippingProps> = props => {
|
|||
const handleChange = createNonNegativeValueChangeHandler(onChange);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
{intl.formatMessage({
|
||||
id: "3rIMq/",
|
||||
defaultMessage: "Shipping",
|
||||
description: "product shipping",
|
||||
})}
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid variant="uniform">
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label={intl.formatMessage({
|
||||
id: "SUbxSK",
|
||||
defaultMessage: "Weight",
|
||||
description: "product weight",
|
||||
})}
|
||||
error={!!formErrors.weight}
|
||||
helperText={getProductErrorMessage(formErrors.weight, intl)}
|
||||
name="weight"
|
||||
value={data.weight}
|
||||
onChange={handleChange}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
{weightUnit || ""}
|
||||
</InputAdornment>
|
||||
),
|
||||
inputProps: {
|
||||
min: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content>
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label={intl.formatMessage({
|
||||
id: "SUbxSK",
|
||||
defaultMessage: "Weight",
|
||||
description: "product weight",
|
||||
})}
|
||||
error={!!formErrors.weight}
|
||||
helperText={getProductErrorMessage(formErrors.weight, intl)}
|
||||
name="weight"
|
||||
type="number"
|
||||
value={data.weight}
|
||||
onChange={handleChange}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">{weightUnit || ""}</InputAdornment>
|
||||
),
|
||||
inputProps: {
|
||||
min: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</DashboardCard.Content>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
ProductShipping.displayName = "ProductShipping";
|
||||
export default ProductShipping;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from "./ProductShipping";
|
|
@ -2,11 +2,8 @@ import {
|
|||
ChannelData,
|
||||
ChannelPriceAndPreorderArgs,
|
||||
} from "@dashboard/channels/utils";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import ControlledCheckbox from "@dashboard/components/ControlledCheckbox";
|
||||
import { DashboardCard } from "@dashboard/components/Card";
|
||||
import { DateTimeTimezoneField } from "@dashboard/components/DateTimeTimezoneField";
|
||||
import FormSpacer from "@dashboard/components/FormSpacer";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import Link from "@dashboard/components/Link";
|
||||
import PreviewPill from "@dashboard/components/PreviewPill";
|
||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
||||
|
@ -17,22 +14,20 @@ import { sectionNames } from "@dashboard/intl";
|
|||
import { renderCollection } from "@dashboard/misc";
|
||||
import { getFormErrors, getProductErrorMessage } from "@dashboard/utils/errors";
|
||||
import createNonNegativeValueChangeHandler from "@dashboard/utils/handlers/nonNegativeValueChangeHandler";
|
||||
import { Table, TableBody, TableCell, TableHead } from "@material-ui/core";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
ClickAwayListener,
|
||||
Grow,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Popper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { Button, DeleteIcon, IconButton, PlusIcon } from "@saleor/macaw-ui";
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
Input,
|
||||
List,
|
||||
PlusIcon,
|
||||
sprinkles,
|
||||
Text,
|
||||
TrashBinIcon,
|
||||
vars,
|
||||
} from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
|
@ -40,7 +35,6 @@ import { ProductCreateData } from "../ProductCreatePage";
|
|||
import { ProductVariantCreateData } from "../ProductVariantCreatePage/form";
|
||||
import { ProductVariantUpdateData } from "../ProductVariantPage/form";
|
||||
import { messages } from "./messages";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
export interface ProductStockFormsetData {
|
||||
quantityAllocated: number;
|
||||
|
@ -84,7 +78,7 @@ export interface ProductStocksProps {
|
|||
onWarehouseConfigure: () => void;
|
||||
}
|
||||
|
||||
const ProductStocks: React.FC<ProductStocksProps> = ({
|
||||
export const ProductStocks: React.FC<ProductStocksProps> = ({
|
||||
data,
|
||||
disabled,
|
||||
hasVariants,
|
||||
|
@ -102,11 +96,8 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
|
|||
onWarehouseStockDelete,
|
||||
onWarehouseConfigure,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const intl = useIntl();
|
||||
const anchor = React.useRef<HTMLDivElement>();
|
||||
const [lastStockRowFocus, setLastStockRowFocus] = React.useState(false);
|
||||
const [isExpanded, setExpansionState] = React.useState(false);
|
||||
const unitsLeft = parseInt(data.globalThreshold, 10) - data.globalSoldUnits;
|
||||
|
||||
const warehousesToAssign =
|
||||
|
@ -141,131 +132,129 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle title={intl.formatMessage(messages.title)} />
|
||||
<CardContent>
|
||||
<div className={classes.skuInputContainer}>
|
||||
<TextField
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
{intl.formatMessage(messages.title)}
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content>
|
||||
<Box __width="50%">
|
||||
<Input
|
||||
disabled={disabled}
|
||||
error={!!formErrors.sku}
|
||||
fullWidth
|
||||
helperText={getProductErrorMessage(formErrors.sku, intl)}
|
||||
label={intl.formatMessage(messages.sku)}
|
||||
name="sku"
|
||||
onChange={handleChange}
|
||||
value={data.sku}
|
||||
data-test-id="sku"
|
||||
size="small"
|
||||
helperText={getProductErrorMessage(formErrors.sku, intl)}
|
||||
/>
|
||||
</div>
|
||||
<ControlledCheckbox
|
||||
checked={data.isPreorder}
|
||||
name="isPreorder"
|
||||
onChange={
|
||||
onEndPreorderTrigger && data.isPreorder
|
||||
? onEndPreorderTrigger
|
||||
: onFormDataChange
|
||||
}
|
||||
disabled={disabled}
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage {...messages.variantInPreorder} />
|
||||
<PreviewPill className={classes.preview} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{!data.isPreorder && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<ControlledCheckbox
|
||||
<Box paddingY={5} display="grid" gap={5}>
|
||||
<Checkbox
|
||||
checked={data.isPreorder}
|
||||
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}
|
||||
name="trackInventory"
|
||||
onChange={onFormDataChange}
|
||||
disabled={disabled}
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage {...messages.trackInventory} />
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage {...messages.trackInventoryDescription} />
|
||||
</Typography>
|
||||
</>
|
||||
onCheckedChange={value =>
|
||||
onFormDataChange({ target: { name: "trackInventory", value } })
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
<Hr />
|
||||
{!data.isPreorder && (
|
||||
<CardContent className={classes.quantityContainer}>
|
||||
<Typography>
|
||||
<div className={classes.quantityHeader}>
|
||||
<span>
|
||||
<FormattedMessage {...messages.quantity} />
|
||||
</span>
|
||||
</div>
|
||||
</Typography>
|
||||
{!productVariantChannelListings?.length && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage {...messages.noChannelWarehousesAllocation} />
|
||||
</Typography>
|
||||
</>
|
||||
>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Text>
|
||||
<FormattedMessage {...messages.trackInventory} />
|
||||
</Text>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage {...messages.trackInventoryDescription} />
|
||||
</Text>
|
||||
</Box>
|
||||
</Checkbox>
|
||||
)}
|
||||
|
||||
{!warehouses?.length && (
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
className={classes.noWarehouseInfo}
|
||||
>
|
||||
{hasVariants ? (
|
||||
<>
|
||||
<FormattedMessage
|
||||
{...messages.configureWarehouseForVariant}
|
||||
values={{
|
||||
a: chunks => (
|
||||
<Link onClick={onWarehouseConfigure}>{chunks}</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormattedMessage
|
||||
{...messages.configureWarehouseForProduct}
|
||||
values={{
|
||||
a: chunks => (
|
||||
<Link onClick={onWarehouseConfigure}>{chunks}</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{!data.isPreorder && (
|
||||
<Box display="grid" gap={5}>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Text>
|
||||
<FormattedMessage {...messages.quantity} />
|
||||
</Text>
|
||||
{!productVariantChannelListings?.length && (
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage
|
||||
{...messages.noChannelWarehousesAllocation}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{!warehouses?.length && (
|
||||
<Text color="textNeutralSubdued">
|
||||
{hasVariants ? (
|
||||
<FormattedMessage
|
||||
{...messages.configureWarehouseForVariant}
|
||||
values={{
|
||||
a: chunks => (
|
||||
<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 &&
|
||||
warehouses?.length > 0 &&
|
||||
!data.isPreorder && (
|
||||
<Table>
|
||||
<colgroup>
|
||||
<col className={classes.colName} />
|
||||
<col className={classes.colQuantity} />
|
||||
<col className={classes.colQuantity} />
|
||||
</colgroup>
|
||||
<TableHead>
|
||||
<TableRowLink>
|
||||
<TableCell className={classes.colName}>
|
||||
<FormattedMessage {...messages.warehouseName} />
|
||||
<TableCell style={{ paddingLeft: vars.space[9] }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage {...messages.warehouseName} />
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colQuantity}>
|
||||
<FormattedMessage {...messages.allocated} />
|
||||
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage {...messages.allocated} />
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colQuantity}>
|
||||
<FormattedMessage {...messages.quantity} />
|
||||
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage {...messages.quantity} />
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colAction} />
|
||||
<TableCell />
|
||||
</TableRowLink>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
@ -277,195 +266,192 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
|
|||
|
||||
return (
|
||||
<TableRowLink key={stock.id}>
|
||||
<TableCell className={classes.colName}>
|
||||
{stock.label}
|
||||
<TableCell style={{ paddingLeft: vars.space[9] }}>
|
||||
<Text>{stock.label}</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colQuantity}>
|
||||
{stock.data?.quantityAllocated || 0}
|
||||
<TableCell>
|
||||
<Text>{stock.data?.quantityAllocated || 0}</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colQuantity}>
|
||||
<TextField
|
||||
<TableCell>
|
||||
<Input
|
||||
data-test-id="stock-input"
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
inputProps={{
|
||||
className: classes.input,
|
||||
min: 0,
|
||||
type: "number",
|
||||
}}
|
||||
onChange={handleQuantityChange}
|
||||
value={stock.value}
|
||||
inputRef={input =>
|
||||
size="small"
|
||||
type="number"
|
||||
min={0}
|
||||
ref={input =>
|
||||
stocks.length === index + 1 &&
|
||||
handleStockInputFocus(input)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colAction}>
|
||||
<IconButton
|
||||
<TableCell>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
color="primary"
|
||||
icon={<TrashBinIcon />}
|
||||
onClick={() => onWarehouseStockDelete(stock.id)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
);
|
||||
})}
|
||||
{warehousesToAssign.length > 0 && (
|
||||
<ClickAwayListener onClickAway={() => setExpansionState(false)}>
|
||||
<TableRowLink
|
||||
className={classes.addRow}
|
||||
onClick={() => setExpansionState(!isExpanded)}
|
||||
>
|
||||
<TableCell colSpan={3} className={classes.actionableText}>
|
||||
<Typography variant="body2">
|
||||
<FormattedMessage {...messages.assignWarehouse} />
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colAction}>
|
||||
<div ref={anchor}>
|
||||
<IconButton
|
||||
data-test-id="add-warehouse"
|
||||
color="primary"
|
||||
<Dropdown>
|
||||
<Dropdown.Trigger>
|
||||
<TableRowLink className={sprinkles({ cursor: "pointer" })}>
|
||||
<TableCell
|
||||
colSpan={3}
|
||||
style={{ paddingLeft: vars.space[9] }}
|
||||
>
|
||||
<Text>
|
||||
<FormattedMessage {...messages.assignWarehouse} />
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell style={{ paddingRight: vars.space[9] }}>
|
||||
<Button
|
||||
type="button"
|
||||
icon={<PlusIcon />}
|
||||
variant="secondary"
|
||||
className={classes.actionableText}
|
||||
>
|
||||
<PlusIcon />
|
||||
</IconButton>
|
||||
<Popper
|
||||
className={classes.popper}
|
||||
open={isExpanded}
|
||||
anchorEl={anchor.current}
|
||||
transition
|
||||
placement="top-end"
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: "right top",
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content align="end">
|
||||
<Box>
|
||||
<List
|
||||
padding={5}
|
||||
borderRadius={4}
|
||||
boxShadow="overlay"
|
||||
backgroundColor="surfaceNeutralPlain"
|
||||
>
|
||||
{warehousesToAssign.map(warehouse => (
|
||||
<Dropdown.Item key={warehouse.id}>
|
||||
<List.Item
|
||||
paddingX={4}
|
||||
paddingY={5}
|
||||
borderRadius={4}
|
||||
onClick={() =>
|
||||
handleWarehouseStockAdd(warehouse.id)
|
||||
}
|
||||
>
|
||||
<Paper className={classes.paper} elevation={8}>
|
||||
{warehousesToAssign.map(warehouse => (
|
||||
<MenuItem
|
||||
className={classes.menuItem}
|
||||
onClick={() =>
|
||||
handleWarehouseStockAdd(warehouse.id)
|
||||
}
|
||||
>
|
||||
{warehouse.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
</ClickAwayListener>
|
||||
<Text>{warehouse.name}</Text>
|
||||
</List.Item>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Dropdown.Content>
|
||||
</Dropdown>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{data.isPreorder && (
|
||||
<CardContent>
|
||||
<Typography variant="caption" className={classes.caption}>
|
||||
<FormattedMessage {...messages.preorderEndDateSetup} />
|
||||
</Typography>
|
||||
|
||||
{data.hasPreorderEndDate && (
|
||||
<div className={classes.dateTimeInputs}>
|
||||
<DateTimeTimezoneField
|
||||
name={"preorderEndDateTime"}
|
||||
<DashboardCard.Content>
|
||||
<Box display="grid" gap={5}>
|
||||
<Text variant="caption">
|
||||
<FormattedMessage {...messages.preorderEndDateSetup} />
|
||||
</Text>
|
||||
{data.hasPreorderEndDate && (
|
||||
<Box>
|
||||
<DateTimeTimezoneField
|
||||
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}
|
||||
futureDatesOnly
|
||||
fullWidth={false}
|
||||
error={localFormErrors.preorderEndDateTime}
|
||||
value={data?.preorderEndDateTime}
|
||||
onChange={event =>
|
||||
onChangePreorderEndDate({
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onFormDataChange({
|
||||
target: {
|
||||
name: "preorderEndDateTime",
|
||||
value: event,
|
||||
name: "hasPreorderEndDate",
|
||||
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
|
||||
? intl.formatMessage(messages.preorderTresholdUnitsLeft, {
|
||||
unitsLeft,
|
||||
})
|
||||
: intl.formatMessage(messages.preorderTresholdUnlimited)}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
{data.hasPreorderEndDate
|
||||
? intl.formatMessage(messages.endDateCancel)
|
||||
: intl.formatMessage(messages.endDateSetup)}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box display="grid" gap={3} paddingTop={5}>
|
||||
<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 && (
|
||||
<Table>
|
||||
<colgroup>
|
||||
<col className={classes.colName} />
|
||||
<col className={classes.colSoldUnits} />
|
||||
<col className={classes.colThreshold} />
|
||||
</colgroup>
|
||||
<TableHead>
|
||||
<TableRowLink>
|
||||
<TableCell className={classes.colName}>
|
||||
<FormattedMessage {...sectionNames.channels} />
|
||||
<TableCell style={{ paddingLeft: vars.space[9] }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage {...sectionNames.channels} />
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colSoldUnits}>
|
||||
<FormattedMessage {...messages.soldUnits} />
|
||||
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage {...messages.soldUnits} />
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colThreshold}>
|
||||
<FormattedMessage {...messages.channelTreshold} />
|
||||
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage {...messages.channelTreshold} />
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
</TableHead>
|
||||
|
@ -477,25 +463,18 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
|
|||
|
||||
return (
|
||||
<TableRowLink key={listing.id}>
|
||||
<TableCell className={classes.colName}>
|
||||
{listing.name}
|
||||
<TableCell style={{ paddingLeft: vars.space[9] }}>
|
||||
<Text>{listing.name}</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colQuantity}>
|
||||
{listing?.unitsSold || 0}
|
||||
<TableCell>
|
||||
<Text>{listing?.unitsSold || 0}</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colQuantity}>
|
||||
<TextField
|
||||
<TableCell>
|
||||
<Input
|
||||
min={0}
|
||||
type="number"
|
||||
name="channel-threshold"
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
inputProps={{
|
||||
className: classes.input,
|
||||
min: 0,
|
||||
type: "number",
|
||||
}}
|
||||
placeholder={intl.formatMessage(
|
||||
messages.preorderTresholdUnlimited,
|
||||
)}
|
||||
onChange={e => {
|
||||
onVariantChannelListingChange(listing.id, {
|
||||
costPrice: listing.costPrice,
|
||||
|
@ -507,6 +486,10 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
|
|||
});
|
||||
}}
|
||||
value={listing?.preorderThreshold ?? ""}
|
||||
size="small"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.preorderTresholdUnlimited,
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
|
@ -515,9 +498,6 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
|
|||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</Card>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
|
||||
ProductStocks.displayName = "ProductStocks";
|
||||
export default ProductStocks;
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export * from "./ProductStocks";
|
||||
export { default } from "./ProductStocks";
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
);
|
|
@ -56,7 +56,6 @@ const props: ProductUpdatePageProps = {
|
|||
onSubmit: () => undefined,
|
||||
onVariantShow: () => undefined,
|
||||
refetch: () => undefined,
|
||||
placeholderImage,
|
||||
product,
|
||||
referencePages: [],
|
||||
referenceProducts: [],
|
||||
|
|
|
@ -66,7 +66,6 @@ const props: ProductUpdatePageProps = {
|
|||
onMediaUrlUpload: () => undefined,
|
||||
onSubmit,
|
||||
onVariantShow: () => undefined,
|
||||
placeholderImage,
|
||||
product,
|
||||
referencePages: [],
|
||||
referenceProducts: [],
|
||||
|
|
|
@ -10,15 +10,14 @@ import {
|
|||
import { ChannelData } from "@dashboard/channels/utils";
|
||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
|
||||
import Attributes, { AttributeInput } from "@dashboard/components/Attributes";
|
||||
import CardMenu from "@dashboard/components/CardMenu";
|
||||
import { AttributeInput, Attributes } from "@dashboard/components/Attributes";
|
||||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import ChannelsAvailabilityCard from "@dashboard/components/ChannelsAvailabilityCard";
|
||||
import { useDevModeContext } from "@dashboard/components/DevModePanel/hooks";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import SeoForm from "@dashboard/components/SeoForm";
|
||||
import { SeoForm } from "@dashboard/components/SeoForm";
|
||||
import { Choice } from "@dashboard/components/SingleSelectField";
|
||||
import {
|
||||
ChannelFragment,
|
||||
|
@ -52,9 +51,9 @@ import React from "react";
|
|||
import { useIntl } from "react-intl";
|
||||
|
||||
import { getChoices } from "../../utils/data";
|
||||
import ProductDetailsForm from "../ProductDetailsForm";
|
||||
import { ProductDetailsForm } from "../ProductDetailsForm";
|
||||
import ProductMedia from "../ProductMedia";
|
||||
import ProductOrganization from "../ProductOrganization";
|
||||
import { ProductOrganization } from "../ProductOrganization";
|
||||
import ProductTaxes from "../ProductTaxes";
|
||||
import ProductVariants from "../ProductVariants";
|
||||
import ProductUpdateForm from "./form";
|
||||
|
@ -72,7 +71,6 @@ export interface ProductUpdatePageProps {
|
|||
channelsErrors: ProductChannelListingErrorFragment[];
|
||||
variantListErrors: ProductVariantListError[];
|
||||
errors: UseProductUpdateHandlerError[];
|
||||
placeholderImage: string;
|
||||
collections: RelayToFlat<SearchCollectionsQuery["search"]>;
|
||||
categories: RelayToFlat<SearchCategoriesQuery["search"]>;
|
||||
attributeValues: RelayToFlat<
|
||||
|
@ -139,7 +137,6 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
media,
|
||||
header,
|
||||
limits,
|
||||
placeholderImage,
|
||||
product,
|
||||
saveButtonBarState,
|
||||
variants,
|
||||
|
@ -326,8 +323,8 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
return (
|
||||
<DetailPageLayout>
|
||||
<TopNav href={productListUrl()} title={header}>
|
||||
<CardMenu
|
||||
menuItems={[
|
||||
<TopNav.Menu
|
||||
items={[
|
||||
...extensionMenuItems,
|
||||
{
|
||||
label: intl.formatMessage(messages.openGraphiQL),
|
||||
|
@ -335,7 +332,7 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
testId: "graphiql-redirect",
|
||||
},
|
||||
]}
|
||||
data-test-id="menu"
|
||||
dataTestId="menu"
|
||||
/>
|
||||
</TopNav>
|
||||
|
||||
|
@ -349,7 +346,6 @@ export const ProductUpdatePage: React.FC<ProductUpdatePageProps> = ({
|
|||
<CardSpacer />
|
||||
<ProductMedia
|
||||
media={media}
|
||||
placeholderImage={placeholderImage}
|
||||
onImageDelete={onImageDelete}
|
||||
onImageReorder={onImageReorder}
|
||||
onImageUpload={onImageUpload}
|
||||
|
|
|
@ -4,14 +4,15 @@ import {
|
|||
} from "@dashboard/attributes/utils/data";
|
||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
|
||||
import Attributes, {
|
||||
import {
|
||||
AttributeInput,
|
||||
Attributes,
|
||||
VariantAttributeScope,
|
||||
} from "@dashboard/components/Attributes";
|
||||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import Grid from "@dashboard/components/Grid";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import {
|
||||
ProductErrorWithAttributesFragment,
|
||||
|
@ -30,14 +31,14 @@ import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
|||
import React from "react";
|
||||
import { defineMessages, useIntl } from "react-intl";
|
||||
|
||||
import ProductShipping from "../ProductShipping/ProductShipping";
|
||||
import ProductStocks from "../ProductStocks";
|
||||
import { ProductShipping } from "../ProductShipping";
|
||||
import { ProductStocks } from "../ProductStocks";
|
||||
import { useManageChannels } from "../ProductVariantChannels/useManageChannels";
|
||||
import { VariantChannelsDialog } from "../ProductVariantChannels/VariantChannelsDialog";
|
||||
import ProductVariantCheckoutSettings from "../ProductVariantCheckoutSettings/ProductVariantCheckoutSettings";
|
||||
import ProductVariantName from "../ProductVariantName";
|
||||
import ProductVariantNavigation from "../ProductVariantNavigation";
|
||||
import ProductVariantPrice from "../ProductVariantPrice";
|
||||
import { ProductVariantPrice } from "../ProductVariantPrice";
|
||||
import ProductVariantCreateForm, {
|
||||
ProductVariantCreateData,
|
||||
ProductVariantCreateHandlers,
|
||||
|
|
|
@ -5,15 +5,16 @@ import {
|
|||
import { ChannelPriceData } from "@dashboard/channels/utils";
|
||||
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
|
||||
import AssignAttributeValueDialog from "@dashboard/components/AssignAttributeValueDialog";
|
||||
import Attributes, {
|
||||
import {
|
||||
AttributeInput,
|
||||
Attributes,
|
||||
VariantAttributeScope,
|
||||
} from "@dashboard/components/Attributes";
|
||||
import CardSpacer from "@dashboard/components/CardSpacer";
|
||||
import Grid from "@dashboard/components/Grid";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import { MetadataFormData } from "@dashboard/components/Metadata";
|
||||
import Metadata from "@dashboard/components/Metadata/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import {
|
||||
ProductChannelListingErrorFragment,
|
||||
|
@ -33,8 +34,8 @@ import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
|||
import React from "react";
|
||||
import { defineMessages, useIntl } from "react-intl";
|
||||
|
||||
import ProductShipping from "../ProductShipping/ProductShipping";
|
||||
import ProductStocks, { ProductStockInput } from "../ProductStocks";
|
||||
import { ProductShipping } from "../ProductShipping";
|
||||
import { ProductStockInput, ProductStocks } from "../ProductStocks";
|
||||
import { useManageChannels } from "../ProductVariantChannels/useManageChannels";
|
||||
import { VariantChannelsDialog } from "../ProductVariantChannels/VariantChannelsDialog";
|
||||
import ProductVariantCheckoutSettings from "../ProductVariantCheckoutSettings/ProductVariantCheckoutSettings";
|
||||
|
@ -43,7 +44,7 @@ import ProductVariantMediaSelectDialog from "../ProductVariantImageSelectDialog"
|
|||
import ProductVariantMedia from "../ProductVariantMedia";
|
||||
import ProductVariantName from "../ProductVariantName";
|
||||
import ProductVariantNavigation from "../ProductVariantNavigation";
|
||||
import ProductVariantPrice from "../ProductVariantPrice";
|
||||
import { ProductVariantPrice } from "../ProductVariantPrice";
|
||||
import ProductVariantSetDefault from "../ProductVariantSetDefault";
|
||||
import ProductVariantUpdateForm, {
|
||||
ProductVariantUpdateData,
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
ChannelPriceAndPreorderArgs,
|
||||
ChannelPriceArgs,
|
||||
} from "@dashboard/channels/utils";
|
||||
import CardTitle from "@dashboard/components/CardTitle";
|
||||
import { DashboardCard } from "@dashboard/components/Card";
|
||||
import PriceField from "@dashboard/components/PriceField";
|
||||
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
|
||||
import Skeleton from "@dashboard/components/Skeleton";
|
||||
|
@ -15,47 +15,11 @@ import {
|
|||
getFormChannelErrors,
|
||||
} from "@dashboard/utils/errors";
|
||||
import getProductErrorMessage from "@dashboard/utils/errors/product";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { TableBody, TableCell, TableHead } from "@material-ui/core";
|
||||
import { Text, vars } from "@saleor/macaw-ui/next";
|
||||
import React from "react";
|
||||
import { FormattedMessage, MessageDescriptor, useIntl } from "react-intl";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
() => ({
|
||||
colPrice: {
|
||||
textAlign: "right",
|
||||
verticalAlign: "middle",
|
||||
width: 200,
|
||||
},
|
||||
colType: {
|
||||
fontSize: 14,
|
||||
textAlign: "right",
|
||||
width: 200,
|
||||
},
|
||||
input: {
|
||||
textAlign: "left",
|
||||
},
|
||||
pricingContent: {
|
||||
"&:last-child": {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
},
|
||||
table: {
|
||||
tableLayout: "fixed",
|
||||
},
|
||||
}),
|
||||
{ name: "ProductVariantPrice" },
|
||||
);
|
||||
|
||||
interface ProductVariantPriceProps {
|
||||
ProductVariantChannelListings?: ChannelData[];
|
||||
errors?: ProductChannelListingErrorFragment[];
|
||||
|
@ -70,7 +34,9 @@ interface ProductVariantPriceProps {
|
|||
|
||||
const numberOfColumns = 2;
|
||||
|
||||
const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
|
||||
export const ProductVariantPrice: React.FC<
|
||||
ProductVariantPriceProps
|
||||
> = props => {
|
||||
const {
|
||||
disabled = false,
|
||||
errors = [],
|
||||
|
@ -79,22 +45,21 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
|
|||
onChange,
|
||||
disabledMessage,
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const formErrors = getFormChannelErrors(["price", "costPrice"], errors);
|
||||
|
||||
if (disabled || !ProductVariantChannelListings.length) {
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
{intl.formatMessage({
|
||||
id: "Xm9qOu",
|
||||
defaultMessage: "Pricing",
|
||||
description: "product pricing, section header",
|
||||
})}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography variant="caption">
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content>
|
||||
<Text variant="caption">
|
||||
{intl.formatMessage(
|
||||
disabledMessage || {
|
||||
id: "e48Igh",
|
||||
|
@ -103,54 +68,60 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
|
|||
description: "variant pricing section subtitle",
|
||||
},
|
||||
)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Text>
|
||||
</DashboardCard.Content>
|
||||
</DashboardCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle
|
||||
title={intl.formatMessage({
|
||||
<DashboardCard>
|
||||
<DashboardCard.Title>
|
||||
{intl.formatMessage({
|
||||
id: "Xm9qOu",
|
||||
defaultMessage: "Pricing",
|
||||
description: "product pricing, section header",
|
||||
})}
|
||||
/>
|
||||
<CardContent>
|
||||
<Typography variant="body2">
|
||||
</DashboardCard.Title>
|
||||
<DashboardCard.Content>
|
||||
<Text variant="body">
|
||||
{intl.formatMessage({
|
||||
id: "VvA7ai",
|
||||
defaultMessage:
|
||||
"Channels that don’t have assigned prices will use their parent channel to define the price. Price will be converted to channel’s currency",
|
||||
description: "info text",
|
||||
})}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<ResponsiveTable className={classes.table}>
|
||||
</Text>
|
||||
</DashboardCard.Content>
|
||||
<ResponsiveTable>
|
||||
<TableHead>
|
||||
<TableRowLink>
|
||||
<TableCell>
|
||||
<FormattedMessage
|
||||
id="c8UT0c"
|
||||
defaultMessage="Channel Name"
|
||||
description="tabel column header"
|
||||
/>
|
||||
<TableCell style={{ paddingLeft: vars.space[9] }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage
|
||||
id="c8UT0c"
|
||||
defaultMessage="Channel Name"
|
||||
description="tabel column header"
|
||||
/>
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colType}>
|
||||
<FormattedMessage
|
||||
id="JFtFgc"
|
||||
defaultMessage="Selling Price"
|
||||
description="tabel column header"
|
||||
/>
|
||||
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage
|
||||
id="JFtFgc"
|
||||
defaultMessage="Selling Price"
|
||||
description="tabel column header"
|
||||
/>
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell className={classes.colType}>
|
||||
<FormattedMessage
|
||||
id="2zCmiR"
|
||||
defaultMessage="Cost price"
|
||||
description="tabel column header"
|
||||
/>
|
||||
<TableCell style={{ width: 200, verticalAlign: "middle" }}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
<FormattedMessage
|
||||
id="2zCmiR"
|
||||
defaultMessage="Cost price"
|
||||
description="tabel column header"
|
||||
/>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
</TableHead>
|
||||
|
@ -169,11 +140,12 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
|
|||
|
||||
return (
|
||||
<TableRowLink key={listing?.id || `skeleton-${index}`}>
|
||||
<TableCell>{listing?.name || <Skeleton />}</TableCell>
|
||||
<TableCell className={classes.colPrice}>
|
||||
<TableCell style={{ paddingLeft: vars.space[9] }}>
|
||||
<Text>{listing?.name || <Skeleton />}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{listing ? (
|
||||
<PriceField
|
||||
className={classes.input}
|
||||
error={!!priceError}
|
||||
label={intl.formatMessage({
|
||||
id: "b1zuN9",
|
||||
|
@ -199,10 +171,9 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
|
|||
<Skeleton />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.colPrice}>
|
||||
<TableCell>
|
||||
{listing ? (
|
||||
<PriceField
|
||||
className={classes.input}
|
||||
error={!!costPriceError}
|
||||
label={intl.formatMessage({
|
||||
id: "KQSONM",
|
||||
|
@ -236,18 +207,18 @@ const ProductVariantPrice: React.FC<ProductVariantPriceProps> = props => {
|
|||
() => (
|
||||
<TableRowLink>
|
||||
<TableCell colSpan={numberOfColumns}>
|
||||
<FormattedMessage
|
||||
id="/glQgs"
|
||||
defaultMessage="No channels found"
|
||||
/>
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id="/glQgs"
|
||||
defaultMessage="No channels found"
|
||||
/>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</ResponsiveTable>
|
||||
</Card>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
ProductVariantPrice.displayName = "ProductVariantPrice";
|
||||
export default ProductVariantPrice;
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export { default } from "./ProductVariantPrice";
|
||||
export * from "./ProductVariantPrice";
|
||||
|
|
|
@ -250,7 +250,7 @@ export const getPreorderEndHourFormData = (endDate?: string) =>
|
|||
endDate ? moment(endDate).format("HH:mm") : "";
|
||||
|
||||
export const getSelectedMedia = <
|
||||
T extends Pick<ProductMediaFragment, "id" | "sortOrder">
|
||||
T extends Pick<ProductMediaFragment, "id" | "sortOrder">,
|
||||
>(
|
||||
media: T[] = [],
|
||||
selectedMediaIds: string[],
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import placeholderImg from "@assets/images/placeholder255x255.png";
|
||||
import ActionDialog from "@dashboard/components/ActionDialog";
|
||||
import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext";
|
||||
import { AttributeInput } from "@dashboard/components/Attributes";
|
||||
|
@ -114,10 +113,8 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
|
|||
},
|
||||
});
|
||||
|
||||
const [
|
||||
reorderProductImages,
|
||||
reorderProductImagesOpts,
|
||||
] = useProductMediaReorderMutation({});
|
||||
const [reorderProductImages, reorderProductImagesOpts] =
|
||||
useProductMediaReorderMutation({});
|
||||
|
||||
const [deleteProduct, deleteProductOpts] = useProductDeleteMutation({
|
||||
onCompleted: () => {
|
||||
|
@ -132,25 +129,23 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
|
|||
},
|
||||
});
|
||||
|
||||
const [
|
||||
createProductImage,
|
||||
createProductImageOpts,
|
||||
] = useProductMediaCreateMutation({
|
||||
onCompleted: data => {
|
||||
const imageError = data.productMediaCreate.errors.find(
|
||||
error =>
|
||||
error.field ===
|
||||
("image" as keyof ProductMediaCreateMutationVariables),
|
||||
);
|
||||
if (imageError) {
|
||||
notify({
|
||||
status: "error",
|
||||
title: intl.formatMessage(errorMessages.imgageUploadErrorTitle),
|
||||
text: intl.formatMessage(errorMessages.imageUploadErrorText),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
const [createProductImage, createProductImageOpts] =
|
||||
useProductMediaCreateMutation({
|
||||
onCompleted: data => {
|
||||
const imageError = data.productMediaCreate.errors.find(
|
||||
error =>
|
||||
error.field ===
|
||||
("image" as keyof ProductMediaCreateMutationVariables),
|
||||
);
|
||||
if (imageError) {
|
||||
notify({
|
||||
status: "error",
|
||||
title: intl.formatMessage(errorMessages.imgageUploadErrorTitle),
|
||||
text: intl.formatMessage(errorMessages.imageUploadErrorText),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [deleteProductImage] = useProductMediaDeleteMutation({
|
||||
onCompleted: () =>
|
||||
|
@ -175,28 +170,26 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
|
|||
},
|
||||
});
|
||||
|
||||
const [
|
||||
createProductMedia,
|
||||
createProductMediaOpts,
|
||||
] = useProductMediaCreateMutation({
|
||||
onCompleted: data => {
|
||||
const errors = data.productMediaCreate.errors;
|
||||
const [createProductMedia, createProductMediaOpts] =
|
||||
useProductMediaCreateMutation({
|
||||
onCompleted: data => {
|
||||
const errors = data.productMediaCreate.errors;
|
||||
|
||||
if (errors.length) {
|
||||
errors.map(error =>
|
||||
if (errors.length) {
|
||||
errors.map(error =>
|
||||
notify({
|
||||
status: "error",
|
||||
text: getProductErrorMessage(error, intl),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
notify({
|
||||
status: "error",
|
||||
text: getProductErrorMessage(error, intl),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
notify({
|
||||
status: "success",
|
||||
text: intl.formatMessage(commonMessages.savedChanges),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
status: "success",
|
||||
text: intl.formatMessage(commonMessages.savedChanges),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleMediaUrlUpload = (mediaUrl: string) => {
|
||||
const variables = {
|
||||
|
@ -282,8 +275,9 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
|
|||
);
|
||||
|
||||
const fetchMoreAttributeValues = {
|
||||
hasMore: !!searchAttributeValuesOpts.data?.attribute?.choices?.pageInfo
|
||||
?.hasNextPage,
|
||||
hasMore:
|
||||
!!searchAttributeValuesOpts.data?.attribute?.choices?.pageInfo
|
||||
?.hasNextPage,
|
||||
loading: !!searchAttributeValuesOpts.loading,
|
||||
onFetchMore: loadMoreAttributeValues,
|
||||
};
|
||||
|
@ -316,7 +310,6 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
|
|||
saveButtonBarState={formTransitionState}
|
||||
media={data?.product?.media}
|
||||
header={product?.name}
|
||||
placeholderImage={placeholderImg}
|
||||
product={product}
|
||||
warehouses={warehouses}
|
||||
taxClasses={taxClasses ?? []}
|
||||
|
|
|
@ -3,7 +3,7 @@ import CardSpacer from "@dashboard/components/CardSpacer";
|
|||
import CountryList from "@dashboard/components/CountryList";
|
||||
import Form from "@dashboard/components/Form";
|
||||
import { DetailPageLayout } from "@dashboard/components/Layouts";
|
||||
import Metadata from "@dashboard/components/Metadata/Metadata";
|
||||
import { Metadata } from "@dashboard/components/Metadata/Metadata";
|
||||
import { MultiAutocompleteChoiceType } from "@dashboard/components/MultiAutocompleteSelectField";
|
||||
import Savebar from "@dashboard/components/Savebar";
|
||||
import { SingleAutocompleteChoiceType } from "@dashboard/components/SingleAutocompleteSelectField";
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue