Merge branch 'master' into add-edges-node-map-util
This commit is contained in:
commit
e373ce3eaa
124 changed files with 8421 additions and 3177 deletions
|
@ -1,3 +1,5 @@
|
|||
import { getDefaultAddress } from "./utils/Utils";
|
||||
|
||||
export function markOrderAsPaid(orderId) {
|
||||
const mutation = `mutation{
|
||||
orderMarkAsPaid(id:"${orderId}"){
|
||||
|
@ -23,12 +25,19 @@ export function addProductToOrder(orderId, variantId, quantity = 1) {
|
|||
return cy.sendRequestWithQuery(mutation);
|
||||
}
|
||||
|
||||
export function createDraftOrder(customerId, shippingMethodId, channelId) {
|
||||
export function createDraftOrder(
|
||||
customerId,
|
||||
shippingMethodId,
|
||||
channelId,
|
||||
address
|
||||
) {
|
||||
const mutation = `mutation{
|
||||
draftOrderCreate(input:{
|
||||
user:"${customerId}"
|
||||
shippingMethod:"${shippingMethodId}"
|
||||
channel: "${channelId}"
|
||||
${getDefaultAddress(address, "shippingAddress")}
|
||||
${getDefaultAddress(address, "billingAddress")}
|
||||
}){
|
||||
orderErrors{
|
||||
message
|
||||
|
|
|
@ -6,5 +6,8 @@ export const DRAFT_ORDER_SELECTORS = {
|
|||
selectCustomerOption: "[data-test-type='option']",
|
||||
addShippingCarrierLink: "[data-test-id='add-shipping-carrier']",
|
||||
finalizeButton: "[data-test='button-bar-confirm']",
|
||||
pageHeader: "[data-test-id='page-header']"
|
||||
pageHeader: "[data-test-id='page-header']",
|
||||
editShippingAddress: '[data-test-id="edit-shipping-address"]',
|
||||
editBillingAddress: '[data-test-id="edit-billing-address"]',
|
||||
customerEmail: '[data-test-id="customer-email"]'
|
||||
};
|
||||
|
|
13
cypress/elements/shared/addressForm.js
Normal file
13
cypress/elements/shared/addressForm.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export const ADDRESS_SELECTORS = {
|
||||
firstName: '[name="firstName"]',
|
||||
lastName: '[name="lastName"]',
|
||||
companyName: '[name="companyName"]',
|
||||
phone: '[name="phone"]',
|
||||
streetAddress1: '[name="streetAddress1"]',
|
||||
streetAddress2: '[name="streetAddress2"]',
|
||||
city: '[name="city"]',
|
||||
postalCode: '[name="postalCode"]',
|
||||
country: '[data-test-id="address-edit-country-select-field"]',
|
||||
countryArea: '[name="countryArea"]',
|
||||
saveButton: '[data-test-id="order-address-edit-dialog-confirm-button"]'
|
||||
};
|
|
@ -1,26 +1,16 @@
|
|||
{
|
||||
"plAddress": {
|
||||
"companyName": "Test3",
|
||||
"streetAddress1": "Smolna",
|
||||
"streetAddress2": "13/1",
|
||||
"city": "Wrocław",
|
||||
"postalCode": "53-346",
|
||||
"country": "PL",
|
||||
"countryArea": "Dolny Śląsk",
|
||||
"phone": "123456787",
|
||||
"currency": "PLN",
|
||||
"countryFullName": "Poland"
|
||||
},
|
||||
"usAddress": {
|
||||
"companyName": "Test3",
|
||||
"streetAddress1": "Amber Prairie",
|
||||
"streetAddress2": "483",
|
||||
"city": "KIRKSHIRE",
|
||||
"postalCode": "52216",
|
||||
"country": "US",
|
||||
"countryArea": "IA",
|
||||
"currency": "USD",
|
||||
"phone": "+12025550180",
|
||||
"countryFullName": "United States of America"
|
||||
"plAddress": {
|
||||
"firstName": "test",
|
||||
"lastName": "test",
|
||||
"companyName": "Test3",
|
||||
"streetAddress1": "Smolna",
|
||||
"streetAddress2": "13/1",
|
||||
"city": "Wrocław",
|
||||
"postalCode": "53-346",
|
||||
"country": "PL",
|
||||
"countryArea": "Dolny Śląsk",
|
||||
"phone": "123456787",
|
||||
"currency": "PLN",
|
||||
"countryFullName": "Poland"
|
||||
}
|
||||
}
|
|
@ -109,12 +109,12 @@ describe("Homepage analytics", () => {
|
|||
homePageUtils
|
||||
.getOrdersReadyToFulfill(defaultChannel.slug)
|
||||
.as("ordersReadyToFulfill");
|
||||
|
||||
createReadyToFulfillOrder(
|
||||
customerId,
|
||||
shippingMethod.id,
|
||||
defaultChannel.id,
|
||||
createdVariants
|
||||
createdVariants,
|
||||
addresses.plAddress
|
||||
);
|
||||
cy.get("@ordersReadyToFulfill").then(ordersReadyToFulfillBefore => {
|
||||
const allOrdersReadyToFulfill = ordersReadyToFulfillBefore + 1;
|
||||
|
@ -195,7 +195,8 @@ describe("Homepage analytics", () => {
|
|||
customerId,
|
||||
shippingMethod.id,
|
||||
defaultChannel.id,
|
||||
createdVariants
|
||||
createdVariants,
|
||||
addresses.plAddress
|
||||
);
|
||||
|
||||
cy.get("@salesAmount").then(salesAmount => {
|
||||
|
@ -227,7 +228,8 @@ describe("Homepage analytics", () => {
|
|||
customerId,
|
||||
shippingMethod.id,
|
||||
defaultChannel.id,
|
||||
createdVariants
|
||||
createdVariants,
|
||||
addresses.plAddress
|
||||
);
|
||||
|
||||
cy.get("@todaysOrders").then(ordersBefore => {
|
||||
|
|
|
@ -23,6 +23,7 @@ describe("Draft orders", () => {
|
|||
|
||||
let defaultChannel;
|
||||
let warehouse;
|
||||
let address;
|
||||
|
||||
before(() => {
|
||||
cy.clearSessionData().loginUserViaRequest();
|
||||
|
@ -38,6 +39,7 @@ describe("Draft orders", () => {
|
|||
cy.fixture("addresses");
|
||||
})
|
||||
.then(addresses => {
|
||||
address = addresses.plAddress;
|
||||
createCustomer(
|
||||
`${randomName}@example.com`,
|
||||
randomName,
|
||||
|
@ -81,7 +83,7 @@ describe("Draft orders", () => {
|
|||
.get(ORDERS_SELECTORS.createOrder)
|
||||
.click();
|
||||
selectChannelInPicker(defaultChannel.name);
|
||||
finalizeDraftOrder(randomName).then(draftOrderNumber => {
|
||||
finalizeDraftOrder(randomName, address).then(draftOrderNumber => {
|
||||
cy.visit(urlList.orders);
|
||||
cy.contains(ORDERS_SELECTORS.orderRow, draftOrderNumber).should(
|
||||
$order => {
|
||||
|
|
|
@ -26,6 +26,7 @@ describe("Orders", () => {
|
|||
let warehouse;
|
||||
let shippingMethod;
|
||||
let variantsList;
|
||||
let address;
|
||||
|
||||
before(() => {
|
||||
cy.clearSessionData().loginUserViaRequest();
|
||||
|
@ -33,8 +34,6 @@ describe("Orders", () => {
|
|||
deleteShippingStartsWith(startsWith);
|
||||
productsUtils.deleteProductsStartsWith(startsWith);
|
||||
|
||||
let address;
|
||||
|
||||
getDefaultChannel()
|
||||
.then(channel => {
|
||||
defaultChannel = channel;
|
||||
|
@ -91,7 +90,7 @@ describe("Orders", () => {
|
|||
.get(ORDERS_SELECTORS.createOrder)
|
||||
.click();
|
||||
selectChannelInPicker(defaultChannel.name);
|
||||
finalizeDraftOrder(randomName).then(draftOrderNumber => {
|
||||
finalizeDraftOrder(randomName, address).then(draftOrderNumber => {
|
||||
cy.visit(urlList.orders);
|
||||
cy.contains(ORDERS_SELECTORS.orderRow, draftOrderNumber).click();
|
||||
cy.contains(ORDERS_SELECTORS.salesChannel, defaultChannel.name).should(
|
||||
|
@ -104,7 +103,8 @@ describe("Orders", () => {
|
|||
customerId: customer.id,
|
||||
channelId: defaultChannel.id,
|
||||
shippingMethodId: shippingMethod.id,
|
||||
variantsList
|
||||
variantsList,
|
||||
address
|
||||
}).then(order => {
|
||||
cy.visit(urlList.orders);
|
||||
cy.contains(ORDERS_SELECTORS.orderRow, order.number).click();
|
||||
|
|
|
@ -1,42 +1,47 @@
|
|||
import { ASSIGN_PRODUCTS_SELECTORS } from "../elements/catalog/products/assign-products-selectors";
|
||||
import { DRAFT_ORDER_SELECTORS } from "../elements/orders/draft-order-selectors";
|
||||
import { SELECT_SHIPPING_METHOD_FORM } from "../elements/shipping/select-shipping-method-form";
|
||||
import { fillUpAddressForm } from "./shared/addressForm";
|
||||
|
||||
export function finalizeDraftOrder(name) {
|
||||
return cy
|
||||
.get(DRAFT_ORDER_SELECTORS.addProducts)
|
||||
export function finalizeDraftOrder(name, address) {
|
||||
cy.get(DRAFT_ORDER_SELECTORS.addProducts)
|
||||
.click()
|
||||
.get(ASSIGN_PRODUCTS_SELECTORS.searchInput)
|
||||
.type(name)
|
||||
.then(() => {
|
||||
cy.contains(ASSIGN_PRODUCTS_SELECTORS.tableRow, name)
|
||||
.find(ASSIGN_PRODUCTS_SELECTORS.checkbox)
|
||||
.click()
|
||||
.get(ASSIGN_PRODUCTS_SELECTORS.submitButton)
|
||||
.click()
|
||||
.get(DRAFT_ORDER_SELECTORS.editCustomerButton)
|
||||
.click()
|
||||
.get(DRAFT_ORDER_SELECTORS.selectCustomer)
|
||||
.type(name);
|
||||
cy.contains(DRAFT_ORDER_SELECTORS.selectCustomerOption, name)
|
||||
.click()
|
||||
.get(DRAFT_ORDER_SELECTORS.addShippingCarrierLink)
|
||||
.click()
|
||||
.get(SELECT_SHIPPING_METHOD_FORM.selectShippingMethod)
|
||||
.click()
|
||||
.get(SELECT_SHIPPING_METHOD_FORM.shippingMethodOption)
|
||||
.first()
|
||||
.click();
|
||||
cy.addAliasToGraphRequest("OrderShippingMethodUpdate")
|
||||
.get(SELECT_SHIPPING_METHOD_FORM.submitButton)
|
||||
.click();
|
||||
cy.wait("@OrderShippingMethodUpdate");
|
||||
cy.getTextFromElement(DRAFT_ORDER_SELECTORS.pageHeader).as(
|
||||
"draftOrderNumber"
|
||||
);
|
||||
cy.addAliasToGraphRequest("OrderDraftFinalize");
|
||||
cy.get(DRAFT_ORDER_SELECTORS.finalizeButton).click();
|
||||
cy.wait("@OrderDraftFinalize");
|
||||
return cy.get("@draftOrderNumber");
|
||||
});
|
||||
.type(name);
|
||||
cy.contains(ASSIGN_PRODUCTS_SELECTORS.tableRow, name)
|
||||
.find(ASSIGN_PRODUCTS_SELECTORS.checkbox)
|
||||
.click()
|
||||
.get(ASSIGN_PRODUCTS_SELECTORS.submitButton)
|
||||
.click()
|
||||
.get(DRAFT_ORDER_SELECTORS.editCustomerButton)
|
||||
.click()
|
||||
.get(DRAFT_ORDER_SELECTORS.selectCustomer)
|
||||
.type(name);
|
||||
cy.contains(DRAFT_ORDER_SELECTORS.selectCustomerOption, name)
|
||||
.click()
|
||||
.get(DRAFT_ORDER_SELECTORS.customerEmail)
|
||||
.should("be.visible")
|
||||
.get(DRAFT_ORDER_SELECTORS.editShippingAddress)
|
||||
.click();
|
||||
fillUpAddressForm(address);
|
||||
cy.get(DRAFT_ORDER_SELECTORS.editBillingAddress).click();
|
||||
fillUpAddressForm(address);
|
||||
cy.get(DRAFT_ORDER_SELECTORS.addShippingCarrierLink)
|
||||
.click()
|
||||
.get(SELECT_SHIPPING_METHOD_FORM.selectShippingMethod)
|
||||
.click()
|
||||
.get(SELECT_SHIPPING_METHOD_FORM.shippingMethodOption)
|
||||
.first()
|
||||
.click();
|
||||
cy.addAliasToGraphRequest("OrderShippingMethodUpdate")
|
||||
.get(SELECT_SHIPPING_METHOD_FORM.submitButton)
|
||||
.click();
|
||||
cy.wait("@OrderShippingMethodUpdate");
|
||||
cy.getTextFromElement(DRAFT_ORDER_SELECTORS.pageHeader).as(
|
||||
"draftOrderNumber"
|
||||
);
|
||||
cy.addAliasToGraphRequest("OrderDraftFinalize");
|
||||
cy.get(DRAFT_ORDER_SELECTORS.finalizeButton).click();
|
||||
cy.wait("@OrderDraftFinalize");
|
||||
return cy.get("@draftOrderNumber");
|
||||
}
|
||||
|
|
26
cypress/steps/shared/addressForm.js
Normal file
26
cypress/steps/shared/addressForm.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { ADDRESS_SELECTORS } from "../../elements/shared/addressForm";
|
||||
import { fillAutocompleteSelect } from "./autocompleteSelect";
|
||||
|
||||
export function fillUpAddressForm(address) {
|
||||
cy.get(ADDRESS_SELECTORS.firstName)
|
||||
.type(address.firstName)
|
||||
.get(ADDRESS_SELECTORS.lastName)
|
||||
.type(address.lastName)
|
||||
.get(ADDRESS_SELECTORS.companyName)
|
||||
.type(address.companyName)
|
||||
.get(ADDRESS_SELECTORS.phone)
|
||||
.type(address.phone)
|
||||
.get(ADDRESS_SELECTORS.streetAddress1)
|
||||
.type(address.streetAddress1)
|
||||
.get(ADDRESS_SELECTORS.streetAddress2)
|
||||
.type(address.streetAddress2)
|
||||
.get(ADDRESS_SELECTORS.city)
|
||||
.type(address.city)
|
||||
.get(ADDRESS_SELECTORS.postalCode)
|
||||
.type(address.postalCode);
|
||||
fillAutocompleteSelect(ADDRESS_SELECTORS.country, address.countryFullName);
|
||||
cy.get(ADDRESS_SELECTORS.countryArea)
|
||||
.type(address.countryArea)
|
||||
.get(ADDRESS_SELECTORS.saveButton)
|
||||
.click();
|
||||
}
|
|
@ -47,11 +47,11 @@ export function createReadyToFulfillOrder(
|
|||
customerId,
|
||||
shippingMethodId,
|
||||
channelId,
|
||||
variantsList
|
||||
variantsList,
|
||||
address
|
||||
) {
|
||||
let order;
|
||||
return orderRequest
|
||||
.createDraftOrder(customerId, shippingMethodId, channelId)
|
||||
return createDraftOrder(customerId, shippingMethodId, channelId, address)
|
||||
.then(orderResp => {
|
||||
order = orderResp;
|
||||
assignVariantsToOrder(order, variantsList);
|
||||
|
@ -64,11 +64,11 @@ export function createOrder({
|
|||
customerId,
|
||||
shippingMethodId,
|
||||
channelId,
|
||||
variantsList
|
||||
variantsList,
|
||||
address
|
||||
}) {
|
||||
let order;
|
||||
return orderRequest
|
||||
.createDraftOrder(customerId, shippingMethodId, channelId)
|
||||
return createDraftOrder(customerId, shippingMethodId, channelId, address)
|
||||
.then(orderResp => {
|
||||
order = orderResp;
|
||||
assignVariantsToOrder(order, variantsList);
|
||||
|
@ -82,12 +82,16 @@ function assignVariantsToOrder(order, variantsList) {
|
|||
orderRequest.addProductToOrder(order.id, variantElement.id);
|
||||
});
|
||||
}
|
||||
export function addPayment(checkoutId) {
|
||||
return checkoutRequest.addPayment(
|
||||
checkoutId,
|
||||
"mirumee.payments.dummy",
|
||||
"not-charged"
|
||||
);
|
||||
|
||||
export function createDraftOrder(
|
||||
customerId,
|
||||
shippingMethodId,
|
||||
channelId,
|
||||
address
|
||||
) {
|
||||
return orderRequest
|
||||
.createDraftOrder(customerId, shippingMethodId, channelId, address)
|
||||
.its("body.data.draftOrderCreate.order");
|
||||
}
|
||||
export function createAndCompleteCheckoutWithoutShipping({
|
||||
channelSlug,
|
||||
|
|
|
@ -16,6 +16,7 @@ export function getDisplayedColumnArray(columnName) {
|
|||
export function expectProductsSortedBy(columnName, inAscOrder = true) {
|
||||
let sortedProductsArray;
|
||||
let productsArray;
|
||||
cy.get(PRODUCTS_LIST.emptyProductRow).should("not.exist");
|
||||
getDisplayedColumnArray(columnName)
|
||||
.then(productsArrayResp => {
|
||||
productsArray = productsArrayResp;
|
||||
|
|
|
@ -3,10 +3,58 @@
|
|||
"context": "dialog header",
|
||||
"string": "Cancel Order"
|
||||
},
|
||||
"PageTypeDeleteWarningDialog multiple assigned items description": {
|
||||
"context": "PageTypeDeleteWarningDialog multiple assigned items description",
|
||||
"string": "Are you sure you want to delete selected page types? If you remove them you won’t be able to assign them to created pages."
|
||||
},
|
||||
"PageTypeDeleteWarningDialog multiple consent label": {
|
||||
"context": "PageTypeDeleteWarningDialog multiple consent label",
|
||||
"string": "Yes, I want to delete those pages types and assigned pages"
|
||||
},
|
||||
"PageTypeDeleteWarningDialog single assigned items description": {
|
||||
"context": "PageTypeDeleteWarningDialog single assigned items description",
|
||||
"string": "You are about to delete page type <b>{typeName}</b>. It is assigned to {assignedItemsCount} {assignedItemsCount,plural,one{page} other{pages}}. Deleting this page type will also delete those pages. Are you sure you want to do this?"
|
||||
},
|
||||
"PageTypeDeleteWarningDialog single consent label": {
|
||||
"context": "PageTypeDeleteWarningDialog single consent label",
|
||||
"string": "Yes, I want to delete this page type and assigned pages"
|
||||
},
|
||||
"PageTypeDeleteWarningDialog single no assigned items description": {
|
||||
"context": "PageTypeDeleteWarningDialog single no assigned items description",
|
||||
"string": "Are you sure you want to delete <b>{typeName}</b>? If you remove it you won’t be able to assign it to created pages."
|
||||
},
|
||||
"PageTypeDeleteWarningDialog with items multiple description": {
|
||||
"context": "PageTypeDeleteWarningDialog with items multiple description",
|
||||
"string": "You are about to delete multiple page types. Some of them are assigned to pages. Deleting those page types will also delete those pages"
|
||||
},
|
||||
"Previous discount label id": {
|
||||
"context": "Previous discount label",
|
||||
"string": "Previous discount value"
|
||||
},
|
||||
"ProductTypeDeleteWarningDialog multiple assigned items description": {
|
||||
"context": "ProductTypeDeleteWarningDialog multiple assigned items description",
|
||||
"string": "Are you sure you want to delete selected product types? If you remove them you won’t be able to assign them to created products."
|
||||
},
|
||||
"ProductTypeDeleteWarningDialog multiple consent label": {
|
||||
"context": "ProductTypeDeleteWarningDialog multiple consent label",
|
||||
"string": "Yes, I want to delete those products types and assigned products"
|
||||
},
|
||||
"ProductTypeDeleteWarningDialog single assigned items description": {
|
||||
"context": "ProductTypeDeleteWarningDialog single assigned items description",
|
||||
"string": "You are about to delete product type <b>{typeName}</b>. It is assigned to {assignedItemsCount} {assignedItemsCount,plural,one{product} other{products}}. Deleting this product type will also delete those products. Are you sure you want to do this?"
|
||||
},
|
||||
"ProductTypeDeleteWarningDialog single consent label": {
|
||||
"context": "ProductTypeDeleteWarningDialog single consent label",
|
||||
"string": "Yes, I want to delete this product type and assigned products"
|
||||
},
|
||||
"ProductTypeDeleteWarningDialog single no assigned items description": {
|
||||
"context": "ProductTypeDeleteWarningDialog single no assigned items description",
|
||||
"string": "Are you sure you want to delete <b>{typeName}</b>? If you remove it you won’t be able to assign it to created products."
|
||||
},
|
||||
"ProductTypeDeleteWarningDialog with items multiple description": {
|
||||
"context": "ProductTypeDeleteWarningDialog with items multiple description",
|
||||
"string": "You are about to delete multiple product types. Some of them are assigned to products. Deleting those product types will also delete those products"
|
||||
},
|
||||
"amount title": {
|
||||
"context": "amount title",
|
||||
"string": "Refunded amount"
|
||||
|
@ -138,6 +186,10 @@
|
|||
"context": "Fixed amount subtitle",
|
||||
"string": "Fixed amount"
|
||||
},
|
||||
"globalConfigPluginMessages title": {
|
||||
"context": "global config plugin status popup title",
|
||||
"string": "Global Plugin"
|
||||
},
|
||||
"homeActivityCardHeader": {
|
||||
"context": "header",
|
||||
"string": "Activity"
|
||||
|
@ -199,6 +251,14 @@
|
|||
"context": "header",
|
||||
"string": "Hello there, {userName}"
|
||||
},
|
||||
"is filter range between value": {
|
||||
"context": "is filter range or value",
|
||||
"string": "between"
|
||||
},
|
||||
"is filter range equal to value": {
|
||||
"context": "is filter range or value",
|
||||
"string": "equal to"
|
||||
},
|
||||
"menuCreateDialogHeader": {
|
||||
"context": "dialog header",
|
||||
"string": "Create Menu"
|
||||
|
@ -322,6 +382,10 @@
|
|||
"pageTypeInputLabel": {
|
||||
"string": "Select content type"
|
||||
},
|
||||
"pluginChannelConfigurationCellMessages per channel": {
|
||||
"context": "PluginChannelConfigurationCell channel title",
|
||||
"string": "Per channel"
|
||||
},
|
||||
"product available for purchase": {
|
||||
"context": "product availability",
|
||||
"string": "Available for purchase"
|
||||
|
@ -2027,23 +2091,19 @@
|
|||
"context": "search",
|
||||
"string": "No results"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_2683154806": {
|
||||
"context": "is filter range or value",
|
||||
"string": "equal to"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_2779594451": {
|
||||
"context": "filter range separator",
|
||||
"string": "and"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_4190318230": {
|
||||
"context": "search results",
|
||||
"string": "Show more"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_773313536": {
|
||||
"context": "is filter range or value",
|
||||
"string": "between"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_996289613": {
|
||||
"context": "button",
|
||||
"string": "Filters"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_FilterContent_dot_2779594451": {
|
||||
"context": "filter range separator",
|
||||
"string": "and"
|
||||
},
|
||||
"src_dot_components_dot_Filter_dot_FilterContent_dot_996289613": {
|
||||
"string": "Filters"
|
||||
},
|
||||
"src_dot_components_dot_ImageUpload_dot_1731007575": {
|
||||
|
@ -2319,6 +2379,18 @@
|
|||
"src_dot_components_dot_SingleSelectField_dot_4205644805": {
|
||||
"string": "No results found"
|
||||
},
|
||||
"src_dot_components_dot_StatusLabel_dot_active": {
|
||||
"context": "status label active",
|
||||
"string": "Active"
|
||||
},
|
||||
"src_dot_components_dot_StatusLabel_dot_deactivated": {
|
||||
"context": "status label deactivated",
|
||||
"string": "Deactivated"
|
||||
},
|
||||
"src_dot_components_dot_StatusLabel_dot_inactive": {
|
||||
"context": "status label inactive",
|
||||
"string": "Inactive"
|
||||
},
|
||||
"src_dot_components_dot_TableHead_dot_868570480": {
|
||||
"string": "Selected {number} items"
|
||||
},
|
||||
|
@ -4532,18 +4604,6 @@
|
|||
"context": "header",
|
||||
"string": "Create Page Type"
|
||||
},
|
||||
"src_dot_pageTypes_dot_components_dot_PageTypeDeleteDialog_dot_2364900868": {
|
||||
"context": "delete page type with its pages",
|
||||
"string": "Page Type you want to delete is used by some pages. Deleting this page type will also delete those pages. Are you sure you want to delete {name}? After doing so you won’t be able to revert changes."
|
||||
},
|
||||
"src_dot_pageTypes_dot_components_dot_PageTypeDeleteDialog_dot_3120835055": {
|
||||
"context": "dialog header",
|
||||
"string": "Delete Page Type"
|
||||
},
|
||||
"src_dot_pageTypes_dot_components_dot_PageTypeDeleteDialog_dot_3734861990": {
|
||||
"context": "delete page type",
|
||||
"string": "Are you sure you want to delete {name}? After doing so you won’t be able to revert changes."
|
||||
},
|
||||
"src_dot_pageTypes_dot_components_dot_PageTypeDetailsPage_dot_1105469372": {
|
||||
"string": "These are general information about this Content Type."
|
||||
},
|
||||
|
@ -4579,6 +4639,14 @@
|
|||
"src_dot_pageTypes_dot_components_dot_PageTypeList_dot_2965257236": {
|
||||
"string": "No page types found"
|
||||
},
|
||||
"src_dot_pageTypes_dot_hooks_dot_usePageTypeDelete_dot_title": {
|
||||
"context": "PageTypeDeleteWarningDialog title",
|
||||
"string": "Delete page {selectedTypesCount,plural,one{type} other{types}}"
|
||||
},
|
||||
"src_dot_pageTypes_dot_hooks_dot_usePageTypeDelete_dot_viewAssignedItemsButtonLabel": {
|
||||
"context": "PageTypeDeleteWarningDialog single assigned items button label",
|
||||
"string": "View pages"
|
||||
},
|
||||
"src_dot_pageTypes_dot_views_dot_2634056946": {
|
||||
"context": "dialog header",
|
||||
"string": "Unassign Attribute from Page Type"
|
||||
|
@ -4887,6 +4955,10 @@
|
|||
"context": "section header",
|
||||
"string": "Authorization"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginDetailsChannelsCard_dot_noChannelsSubtitle": {
|
||||
"context": "PluginDetailsChannelsCard no channels subtitle",
|
||||
"string": "Plugin’s settings are set common across all channels"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginInfo_dot_1049131348": {
|
||||
"context": "plugin name",
|
||||
"string": "Plugin Name"
|
||||
|
@ -4914,21 +4986,10 @@
|
|||
"context": "section header",
|
||||
"string": "Plugin Settings"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsDetailsPage_dot_1970881031": {
|
||||
"context": "section header",
|
||||
"string": "Plugin Settings"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsDetailsPage_dot_3352026836": {
|
||||
"context": "header",
|
||||
"string": "{pluginName} Details"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsDetailsPage_dot_3425535100": {
|
||||
"context": "section header",
|
||||
"string": "Plugin Information and Status"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsDetailsPage_dot_3799756739": {
|
||||
"string": "These are general information about your store. They define what is the URL of your store and what is shown in browsers taskbar."
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_3233248823": {
|
||||
"string": "Search Plugins..."
|
||||
},
|
||||
|
@ -4937,28 +4998,56 @@
|
|||
"string": "All Plugins"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_active": {
|
||||
"context": "plugin",
|
||||
"string": "Active"
|
||||
"context": "plugin filters error messages active",
|
||||
"string": "Active is not selected"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_deactivated": {
|
||||
"context": "plugin",
|
||||
"string": "Inactive"
|
||||
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_channelStatusSectionSubtitle": {
|
||||
"context": "status section subtitle",
|
||||
"string": "Channel status"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_3247064221": {
|
||||
"context": "plugin status",
|
||||
"string": "Active"
|
||||
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_channelStatusSectionTitle": {
|
||||
"context": "status section title",
|
||||
"string": "Status in channel"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_4120604650": {
|
||||
"context": "user action bar",
|
||||
"string": "Action"
|
||||
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_channels": {
|
||||
"context": "plugin filters error messages channels",
|
||||
"string": "No channels selected"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_636461959": {
|
||||
"context": "plugin name",
|
||||
"string": "Name"
|
||||
"src_dot_plugins_dot_components_dot_PluginsListPage_dot_configTypeSectionTitle": {
|
||||
"context": "config type section title",
|
||||
"string": "Configuration Type"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_666641390": {
|
||||
"string": "No plugins found"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_channelLabel": {
|
||||
"context": "table header channel col label",
|
||||
"string": "Channel"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_channelTitle": {
|
||||
"context": "plugin channel availability status title",
|
||||
"string": "Active in {activeChannelsCount}"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_confLabel": {
|
||||
"context": "table header configuration col label",
|
||||
"string": "Configuration"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_description": {
|
||||
"context": "global config plugin status popup description",
|
||||
"string": "Global plugins are set across all channels in your ecommerce. Only status is shown for those types of plugins"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_globalLabel": {
|
||||
"context": "PluginChannelConfigurationCell global title",
|
||||
"string": "Global"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_nameLabel": {
|
||||
"context": "table header name col label",
|
||||
"string": "Name"
|
||||
},
|
||||
"src_dot_plugins_dot_components_dot_PluginsList_dot_title": {
|
||||
"context": "channel config plugin status popup title",
|
||||
"string": "Assigned to {activeChannelsCount} of {allChannelsCount} channels"
|
||||
},
|
||||
"src_dot_plugins_dot_views_dot_1096544448": {
|
||||
"context": "header",
|
||||
"string": "Authorization Field Delete"
|
||||
|
@ -4992,14 +5081,6 @@
|
|||
"context": "section header",
|
||||
"string": "Variant Attributes"
|
||||
},
|
||||
"src_dot_productTypes_dot_components_dot_ProductTypeDeleteDialog_dot_2297471173": {
|
||||
"context": "delete product type",
|
||||
"string": "Are you sure you want to delete {name}?"
|
||||
},
|
||||
"src_dot_productTypes_dot_components_dot_ProductTypeDeleteDialog_dot_924066985": {
|
||||
"context": "dialog header",
|
||||
"string": "Delete Product Type"
|
||||
},
|
||||
"src_dot_productTypes_dot_components_dot_ProductTypeDetailsPage_dot_1217376589": {
|
||||
"context": "switch button",
|
||||
"string": "Product type uses Variant Attributes"
|
||||
|
@ -5079,17 +5160,17 @@
|
|||
"src_dot_productTypes_dot_components_dot_ProductTypeShipping_dot_746695941": {
|
||||
"string": "Weight"
|
||||
},
|
||||
"src_dot_productTypes_dot_hooks_dot_useProductTypeDelete_dot_title": {
|
||||
"context": "ProductTypeDeleteWarningDialog title",
|
||||
"string": "Delete product {selectedTypesCount,plural,one{type} other{types}}"
|
||||
},
|
||||
"src_dot_productTypes_dot_hooks_dot_useProductTypeDelete_dot_viewAssignedItemsButtonLabel": {
|
||||
"context": "ProductTypeDeleteWarningDialog single assigned items button label",
|
||||
"string": "View products"
|
||||
},
|
||||
"src_dot_productTypes_dot_views_dot_3822478981": {
|
||||
"string": "Successfully created product type"
|
||||
},
|
||||
"src_dot_productTypes_dot_views_dot_ProductTypeList_dot_4080551769": {
|
||||
"context": "dialog header",
|
||||
"string": "Delete Product Types"
|
||||
},
|
||||
"src_dot_productTypes_dot_views_dot_ProductTypeList_dot_4269954457": {
|
||||
"context": "dialog content",
|
||||
"string": "{counter,plural,one{Are you sure you want to delete this product type?} other{Are you sure you want to delete {displayQuantity} product types?}}"
|
||||
},
|
||||
"src_dot_productTypes_dot_views_dot_ProductTypeUpdate_dot_3512959355": {
|
||||
"string": "Product type deleted"
|
||||
},
|
||||
|
|
|
@ -2820,7 +2820,7 @@ type Mutation {
|
|||
giftCardCreate(input: GiftCardCreateInput!): GiftCardCreate
|
||||
giftCardDeactivate(id: ID!): GiftCardDeactivate
|
||||
giftCardUpdate(id: ID!, input: GiftCardUpdateInput!): GiftCardUpdate
|
||||
pluginUpdate(id: ID!, input: PluginUpdateInput!): PluginUpdate
|
||||
pluginUpdate(channel: ID, id: ID!, input: PluginUpdateInput!): PluginUpdate
|
||||
saleCreate(input: SaleInput!): SaleCreate
|
||||
saleDelete(id: ID!): SaleDelete
|
||||
saleBulkDelete(ids: [ID]!): SaleBulkDelete
|
||||
|
@ -3579,6 +3579,7 @@ enum PageErrorCode {
|
|||
}
|
||||
|
||||
input PageFilterInput {
|
||||
pageTypes: [ID!]
|
||||
search: String
|
||||
metadata: [MetadataInput]
|
||||
}
|
||||
|
@ -3812,6 +3813,7 @@ enum PaymentErrorCode {
|
|||
SHIPPING_METHOD_NOT_SET
|
||||
PAYMENT_ERROR
|
||||
NOT_SUPPORTED_GATEWAY
|
||||
CHANNEL_INACTIVE
|
||||
}
|
||||
|
||||
input PaymentFilterInput {
|
||||
|
@ -3950,14 +3952,25 @@ input PermissionGroupUpdateInput {
|
|||
removeUsers: [ID!]
|
||||
}
|
||||
|
||||
type Plugin implements Node {
|
||||
type Plugin {
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String!
|
||||
globalConfiguration: PluginConfiguration
|
||||
channelConfigurations: [PluginConfiguration!]!
|
||||
}
|
||||
|
||||
type PluginConfiguration {
|
||||
active: Boolean!
|
||||
channel: Channel
|
||||
configuration: [ConfigurationItem]
|
||||
}
|
||||
|
||||
enum PluginConfigurationType {
|
||||
PER_CHANNEL
|
||||
GLOBAL
|
||||
}
|
||||
|
||||
type PluginCountableConnection {
|
||||
pageInfo: PageInfo!
|
||||
edges: [PluginCountableEdge!]!
|
||||
|
@ -3985,8 +3998,9 @@ enum PluginErrorCode {
|
|||
}
|
||||
|
||||
input PluginFilterInput {
|
||||
active: Boolean
|
||||
statusInChannels: PluginStatusInChannelsInput
|
||||
search: String
|
||||
type: PluginConfigurationType
|
||||
}
|
||||
|
||||
enum PluginSortField {
|
||||
|
@ -3999,6 +4013,11 @@ input PluginSortingInput {
|
|||
field: PluginSortField!
|
||||
}
|
||||
|
||||
input PluginStatusInChannelsInput {
|
||||
active: Boolean!
|
||||
channels: [ID!]!
|
||||
}
|
||||
|
||||
type PluginUpdate {
|
||||
plugin: Plugin
|
||||
pluginsErrors: [PluginError!]! @deprecated(reason: "Use errors field instead. This field will be removed in Saleor 4.0.")
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import { filter } from "fuzzaldrin";
|
||||
import React from "react";
|
||||
|
||||
export const useChannelsSearch = function<T extends { name: string }>(
|
||||
channels: T[]
|
||||
) {
|
||||
const [query, onQueryChange] = React.useState("");
|
||||
const filteredChannels = filter<T, "name">(channels, query, { key: "name" });
|
||||
|
||||
return { query, onQueryChange, filteredChannels };
|
||||
};
|
25
src/components/Alert/InlineAlert.tsx
Normal file
25
src/components/Alert/InlineAlert.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import React from "react";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
container: {
|
||||
backgroundColor: theme.palette.error.main,
|
||||
padding: theme.spacing(1.5, 2)
|
||||
}
|
||||
}),
|
||||
{ name: "InlineAlert" }
|
||||
);
|
||||
|
||||
interface AlertCardProps {
|
||||
children?: React.ReactNode | React.ReactNode[];
|
||||
}
|
||||
|
||||
const AlertCard: React.FC<AlertCardProps> = ({ children }) => {
|
||||
const classes = useStyles({});
|
||||
|
||||
return <Card className={classes.container}>{children}</Card>;
|
||||
};
|
||||
|
||||
export default AlertCard;
|
|
@ -53,7 +53,7 @@ export const ChannelsAvailabilityWrapper: React.FC<ChannelsAvailabilityWrapperPr
|
|||
toolbar={
|
||||
<RequirePermissions
|
||||
userPermissions={user?.userPermissions || []}
|
||||
requiredPermissions={[PermissionEnum.MANAGE_CHANNELS]}
|
||||
requiredPermissions={[PermissionEnum.MANAGE_PRODUCTS]}
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import Divider from "@material-ui/core/Divider";
|
||||
import initial from "lodash-es/initial";
|
||||
import React from "react";
|
||||
|
||||
interface CollectionWithDividersProps<T> {
|
||||
DividerComponent?: React.FunctionComponent;
|
||||
renderEmpty?: (collection: T[]) => any;
|
||||
collection: T[];
|
||||
renderItem: (
|
||||
item: T | undefined,
|
||||
index: number | undefined,
|
||||
collection: T[]
|
||||
) => any;
|
||||
}
|
||||
|
||||
function CollectionWithDividers<T>({
|
||||
collection,
|
||||
renderItem,
|
||||
DividerComponent,
|
||||
renderEmpty
|
||||
}: CollectionWithDividersProps<T>) {
|
||||
const hasNoItemsAndPlaceholder = !renderEmpty && !collection.length;
|
||||
|
||||
if (hasNoItemsAndPlaceholder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!collection.length) {
|
||||
return !!renderEmpty ? renderEmpty(collection) : null;
|
||||
}
|
||||
|
||||
const SelectedDividerComponent = DividerComponent || Divider;
|
||||
|
||||
return initial(
|
||||
collection.reduce(
|
||||
(result, item, index) => [
|
||||
...result,
|
||||
renderItem(item, index, collection),
|
||||
<SelectedDividerComponent />
|
||||
],
|
||||
[]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default CollectionWithDividers;
|
2
src/components/CollectionWithDividers/index.tsx
Normal file
2
src/components/CollectionWithDividers/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./CollectionWithDividers";
|
||||
export { default } from "./CollectionWithDividers";
|
47
src/components/DeleteButton/DeleteButton.tsx
Normal file
47
src/components/DeleteButton/DeleteButton.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
button: {
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.error.dark
|
||||
},
|
||||
backgroundColor: theme.palette.error.main,
|
||||
color: theme.palette.error.contrastText
|
||||
}
|
||||
}),
|
||||
{ name: "DeleteButton" }
|
||||
);
|
||||
|
||||
interface DeleteButtonProps {
|
||||
onClick: () => void;
|
||||
label?: string | React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DeleteButton: React.FC<DeleteButtonProps> = ({
|
||||
onClick,
|
||||
label,
|
||||
disabled = false
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={onClick}
|
||||
className={classes.button}
|
||||
data-test="button-bar-delete"
|
||||
disabled={disabled}
|
||||
>
|
||||
{label || intl.formatMessage(buttonMessages.delete)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteButton;
|
2
src/components/DeleteButton/index.tsx
Normal file
2
src/components/DeleteButton/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./DeleteButton";
|
||||
export { default } from "./DeleteButton";
|
|
@ -6,15 +6,17 @@ import { fade } from "@material-ui/core/styles/colorManipulator";
|
|||
import Typography from "@material-ui/core/Typography";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { FilterContent } from ".";
|
||||
import { IFilter, IFilterElement } from "./types";
|
||||
import { FilterErrorMessages, IFilter, IFilterElement } from "./types";
|
||||
import useFilter from "./useFilter";
|
||||
import { extractInvalidFilters } from "./utils";
|
||||
|
||||
export interface FilterProps<TFilterKeys extends string = string> {
|
||||
currencySymbol?: string;
|
||||
errorMessages?: FilterErrorMessages<TFilterKeys>;
|
||||
menu: IFilter<TFilterKeys>;
|
||||
onFilterAdd: (filter: Array<IFilterElement<string>>) => void;
|
||||
}
|
||||
|
@ -87,15 +89,30 @@ const useStyles = makeStyles(
|
|||
{ name: "Filter" }
|
||||
);
|
||||
const Filter: React.FC<FilterProps> = props => {
|
||||
const { currencySymbol, menu, onFilterAdd } = props;
|
||||
const { currencySymbol, menu, onFilterAdd, errorMessages } = props;
|
||||
const classes = useStyles(props);
|
||||
|
||||
const anchor = React.useRef<HTMLDivElement>();
|
||||
const [isFilterMenuOpened, setFilterMenuOpened] = React.useState(false);
|
||||
const [isFilterMenuOpened, setFilterMenuOpened] = useState(false);
|
||||
const [filterErrors, setFilterErrors] = useState<string[]>([]);
|
||||
const [data, dispatch, reset] = useFilter(menu);
|
||||
|
||||
const isFilterActive = menu.some(filterElement => filterElement.active);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const invalidFilters = extractInvalidFilters(data, menu);
|
||||
|
||||
if (!!invalidFilters.length) {
|
||||
const parsedFilterErrors = invalidFilters.map(({ name }) => name);
|
||||
setFilterErrors(parsedFilterErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
setFilterErrors([]);
|
||||
onFilterAdd(data);
|
||||
setFilterMenuOpened(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickAwayListener
|
||||
onClickAway={event => {
|
||||
|
@ -120,10 +137,17 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
<>
|
||||
<span className={classes.separator} />
|
||||
<Typography className={classes.addFilterText}>
|
||||
{menu.reduce(
|
||||
(acc, filterElement) => acc + (filterElement.active ? 1 : 0),
|
||||
0
|
||||
)}
|
||||
{menu.reduce((acc, filterElement) => {
|
||||
const dataFilterElement = data.find(
|
||||
({ name }) => name === filterElement.name
|
||||
);
|
||||
|
||||
if (!dataFilterElement) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc + (dataFilterElement.active ? 1 : 0);
|
||||
}, 0)}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
|
@ -157,14 +181,14 @@ const Filter: React.FC<FilterProps> = props => {
|
|||
}}
|
||||
>
|
||||
<FilterContent
|
||||
errorMessages={errorMessages}
|
||||
errors={filterErrors}
|
||||
dataStructure={menu}
|
||||
currencySymbol={currencySymbol}
|
||||
filters={data}
|
||||
onClear={reset}
|
||||
onFilterPropertyChange={dispatch}
|
||||
onSubmit={() => {
|
||||
onFilterAdd(data);
|
||||
setFilterMenuOpened(false);
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</Grow>
|
||||
)}
|
||||
|
|
|
@ -13,12 +13,16 @@ import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
|
|||
import { FilterBaseFieldProps } from "./types";
|
||||
|
||||
interface FilterAutocompleteFieldProps extends FilterBaseFieldProps {
|
||||
displayValues: Record<string, MultiAutocompleteChoiceType[]>;
|
||||
setDisplayValues: (
|
||||
values: Record<string, MultiAutocompleteChoiceType[]>
|
||||
) => void;
|
||||
displayValues: FilterAutocompleteDisplayValues;
|
||||
setDisplayValues: (values: FilterAutocompleteDisplayValues) => void;
|
||||
initialDisplayValues: FilterAutocompleteDisplayValues;
|
||||
}
|
||||
|
||||
export type FilterAutocompleteDisplayValues = Record<
|
||||
string,
|
||||
MultiAutocompleteChoiceType[]
|
||||
>;
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
hr: {
|
||||
|
@ -29,7 +33,8 @@ const useStyles = makeStyles(
|
|||
padding: "12px 0 9px 12px"
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: theme.spacing(1)
|
||||
marginBottom: theme.spacing(1),
|
||||
paddingTop: theme.spacing(1)
|
||||
},
|
||||
noResults: {
|
||||
marginTop: theme.spacing(1)
|
||||
|
@ -51,11 +56,13 @@ const FilterAutocompleteField: React.FC<FilterAutocompleteFieldProps> = ({
|
|||
filterField,
|
||||
setDisplayValues,
|
||||
onFilterPropertyChange,
|
||||
initialDisplayValues,
|
||||
...rest
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
|
||||
const fieldDisplayValues = displayValues[filterField.name];
|
||||
const initialFieldDisplayValues = initialDisplayValues[filterField.name];
|
||||
const availableOptions = filterField.options.filter(option =>
|
||||
fieldDisplayValues.every(
|
||||
displayValue => displayValue.value !== option.value
|
||||
|
@ -63,11 +70,6 @@ const FilterAutocompleteField: React.FC<FilterAutocompleteFieldProps> = ({
|
|||
);
|
||||
const displayNoResults =
|
||||
availableOptions.length === 0 && fieldDisplayValues.length === 0;
|
||||
const displayHr = !(
|
||||
(fieldDisplayValues.length === 0 && availableOptions.length > 0) ||
|
||||
(availableOptions.length === 0 && fieldDisplayValues.length > 0) ||
|
||||
displayNoResults
|
||||
);
|
||||
|
||||
const handleChange = (option: MultiAutocompleteChoiceType) => {
|
||||
onFilterPropertyChange({
|
||||
|
@ -90,6 +92,19 @@ const FilterAutocompleteField: React.FC<FilterAutocompleteFieldProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
const isValueChecked = (displayValue: MultiAutocompleteChoiceType) =>
|
||||
filterField.value.includes(displayValue.value);
|
||||
|
||||
const filteredValuesChecked = initialFieldDisplayValues.filter(
|
||||
isValueChecked
|
||||
);
|
||||
|
||||
const filteredValuesUnchecked = fieldDisplayValues.filter(
|
||||
displayValue => !isValueChecked(displayValue)
|
||||
);
|
||||
|
||||
const displayHr = !!filteredValuesChecked.length;
|
||||
|
||||
return (
|
||||
<div {...rest}>
|
||||
<TextField
|
||||
|
@ -104,7 +119,7 @@ const FilterAutocompleteField: React.FC<FilterAutocompleteFieldProps> = ({
|
|||
}}
|
||||
onChange={event => filterField.onSearchChange(event.target.value)}
|
||||
/>
|
||||
{fieldDisplayValues.map(displayValue => (
|
||||
{filteredValuesChecked.map(displayValue => (
|
||||
<div className={classes.option} key={displayValue.value}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
|
@ -130,7 +145,7 @@ const FilterAutocompleteField: React.FC<FilterAutocompleteFieldProps> = ({
|
|||
<FormattedMessage defaultMessage="No results" description="search" />
|
||||
</Typography>
|
||||
)}
|
||||
{availableOptions.map(option => (
|
||||
{filteredValuesUnchecked.map(option => (
|
||||
<div
|
||||
className={classes.option}
|
||||
key={option.value}
|
||||
|
|
|
@ -1,433 +0,0 @@
|
|||
import { TextField } from "@material-ui/core";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Radio from "@material-ui/core/Radio";
|
||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import useStateFromProps from "@saleor/hooks/useStateFromProps";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
|
||||
|
||||
import Checkbox from "../Checkbox";
|
||||
import FormSpacer from "../FormSpacer";
|
||||
import Hr from "../Hr";
|
||||
import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
|
||||
import { SingleAutocompleteChoiceType } from "../SingleAutocompleteSelectField";
|
||||
import SingleSelectField from "../SingleSelectField";
|
||||
import Arrow from "./Arrow";
|
||||
import FilterAutocompleteField from "./FilterAutocompleteField";
|
||||
import FilterOptionField from "./FilterOptionField";
|
||||
import { FilterReducerAction } from "./reducer";
|
||||
import { FieldType, FilterType, IFilter } from "./types";
|
||||
|
||||
export interface FilterContentProps<T extends string = string> {
|
||||
filters: IFilter<T>;
|
||||
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||
onClear: () => void;
|
||||
onSubmit: () => void;
|
||||
currencySymbol?: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
actionBar: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: theme.spacing(1, 3)
|
||||
},
|
||||
andLabel: {
|
||||
margin: theme.spacing(0, 2)
|
||||
},
|
||||
arrow: {
|
||||
marginRight: theme.spacing(2)
|
||||
},
|
||||
clear: {
|
||||
marginRight: theme.spacing(1)
|
||||
},
|
||||
filterFieldBar: {
|
||||
"&:not(:last-of-type)": {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`
|
||||
},
|
||||
padding: theme.spacing(1, 2.5)
|
||||
},
|
||||
filterSettings: {
|
||||
background: fade(theme.palette.primary.main, 0.2),
|
||||
padding: theme.spacing(2, 3)
|
||||
},
|
||||
input: {
|
||||
padding: "12px 0 9px 12px"
|
||||
},
|
||||
inputRange: {
|
||||
alignItems: "center",
|
||||
display: "flex"
|
||||
},
|
||||
label: {
|
||||
fontWeight: 600
|
||||
},
|
||||
option: {
|
||||
left: -theme.spacing(0.5),
|
||||
position: "relative"
|
||||
},
|
||||
optionRadio: {
|
||||
left: -theme.spacing(0.25)
|
||||
}
|
||||
}),
|
||||
{ name: "FilterContent" }
|
||||
);
|
||||
|
||||
function getIsFilterMultipleChoices(
|
||||
intl: IntlShape
|
||||
): SingleAutocompleteChoiceType[] {
|
||||
return [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "equal to",
|
||||
description: "is filter range or value"
|
||||
}),
|
||||
value: FilterType.SINGULAR
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "between",
|
||||
description: "is filter range or value"
|
||||
}),
|
||||
value: FilterType.MULTIPLE
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const filterFieldTestingContext = "filter-field";
|
||||
|
||||
const FilterContent: React.FC<FilterContentProps> = ({
|
||||
currencySymbol,
|
||||
filters,
|
||||
onClear,
|
||||
onFilterPropertyChange,
|
||||
onSubmit
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
const [
|
||||
autocompleteDisplayValues,
|
||||
setAutocompleteDisplayValues
|
||||
] = useStateFromProps<Record<string, MultiAutocompleteChoiceType[]>>(
|
||||
filters.reduce((acc, filterField) => {
|
||||
if (filterField.type === FieldType.autocomplete) {
|
||||
acc[filterField.name] = filterField.displayValues;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
<form
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<div className={classes.actionBar}>
|
||||
<Typography className={classes.label}>
|
||||
<FormattedMessage defaultMessage="Filters" />
|
||||
</Typography>
|
||||
<div>
|
||||
<Button
|
||||
data-test="clear"
|
||||
className={classes.clear}
|
||||
onClick={onClear}
|
||||
>
|
||||
<FormattedMessage {...buttonMessages.clear} />
|
||||
</Button>
|
||||
<Button
|
||||
data-test="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage {...buttonMessages.done} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Hr />
|
||||
{filters
|
||||
.filter(filter => !!filter)
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.map(filterField => (
|
||||
<React.Fragment key={filterField.name}>
|
||||
<div className={classes.filterFieldBar}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
data-test="filterGroupActive"
|
||||
data-test-id={filterField.name}
|
||||
checked={filterField.active}
|
||||
/>
|
||||
}
|
||||
label={filterField.label}
|
||||
onChange={() =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
active: !filterField.active
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{filterField.active && (
|
||||
<div className={classes.filterSettings}>
|
||||
{filterField.type === FieldType.text && (
|
||||
<TextField
|
||||
data-test={filterFieldTestingContext}
|
||||
data-test-id={filterField.name}
|
||||
fullWidth
|
||||
name={filterField.name}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
value={filterField.value[0]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [event.target.value, filterField.value[1]]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{[FieldType.date, FieldType.price, FieldType.number].includes(
|
||||
filterField.type
|
||||
) && (
|
||||
<>
|
||||
<SingleSelectField
|
||||
data-test="filterFieldRangeTypeChoice"
|
||||
choices={getIsFilterMultipleChoices(intl)}
|
||||
value={
|
||||
filterField.multiple
|
||||
? FilterType.MULTIPLE
|
||||
: FilterType.SINGULAR
|
||||
}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
multiple:
|
||||
event.target.value === FilterType.MULTIPLE
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<div className={classes.inputRange}>
|
||||
<div>
|
||||
<Arrow className={classes.arrow} />
|
||||
</div>
|
||||
{filterField.multiple ? (
|
||||
<>
|
||||
<TextField
|
||||
data-test={filterFieldTestingContext}
|
||||
data-test-id={filterField.name}
|
||||
data-test-range-type="min"
|
||||
fullWidth
|
||||
name={filterField.name + "_min"}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
endAdornment:
|
||||
filterField.type === FieldType.price &&
|
||||
currencySymbol,
|
||||
type:
|
||||
filterField.type === FieldType.date
|
||||
? "date"
|
||||
: "number"
|
||||
}}
|
||||
value={filterField.value[0]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [
|
||||
event.target.value,
|
||||
filterField.value[1]
|
||||
]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className={classes.andLabel}>
|
||||
<FormattedMessage
|
||||
defaultMessage="and"
|
||||
description="filter range separator"
|
||||
/>
|
||||
</span>
|
||||
<TextField
|
||||
data-test={filterFieldTestingContext}
|
||||
data-test-id={filterField.name}
|
||||
data-test-range-type="max"
|
||||
fullWidth
|
||||
name={filterField.name + "_max"}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
endAdornment:
|
||||
filterField.type === FieldType.price &&
|
||||
currencySymbol,
|
||||
type:
|
||||
filterField.type === FieldType.date
|
||||
? "date"
|
||||
: "number"
|
||||
}}
|
||||
value={filterField.value[1]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [
|
||||
filterField.value[0],
|
||||
event.target.value
|
||||
]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<TextField
|
||||
data-test={filterFieldTestingContext}
|
||||
data-test-id={filterField.name}
|
||||
fullWidth
|
||||
name={filterField.name}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
endAdornment:
|
||||
filterField.type === FieldType.price &&
|
||||
currencySymbol,
|
||||
type:
|
||||
filterField.type === FieldType.date
|
||||
? "date"
|
||||
: [
|
||||
FieldType.number,
|
||||
FieldType.price
|
||||
].includes(filterField.type)
|
||||
? "number"
|
||||
: "text"
|
||||
}}
|
||||
value={filterField.value[0]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [
|
||||
event.target.value,
|
||||
filterField.value[1]
|
||||
]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{filterField.type === FieldType.options && (
|
||||
<FilterOptionField
|
||||
data-test={filterFieldTestingContext}
|
||||
data-test-id={filterField.name}
|
||||
filterField={filterField}
|
||||
onFilterPropertyChange={onFilterPropertyChange}
|
||||
/>
|
||||
)}
|
||||
{filterField.type === FieldType.boolean &&
|
||||
filterField.options.map(option => (
|
||||
<div
|
||||
className={classNames(
|
||||
classes.option,
|
||||
classes.optionRadio
|
||||
)}
|
||||
key={option.value}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Radio
|
||||
data-test="filterFieldBoolean"
|
||||
data-test-is-checked={
|
||||
filterField.value[0] === option.value
|
||||
}
|
||||
checked={filterField.value[0] === option.value}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={option.label}
|
||||
name={filterField.name}
|
||||
onChange={() =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filterField.name,
|
||||
update: {
|
||||
value: [option.value]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{filterField.type === FieldType.autocomplete &&
|
||||
filterField.multiple && (
|
||||
<FilterAutocompleteField
|
||||
data-test={filterFieldTestingContext}
|
||||
data-test-id={filterField.name}
|
||||
displayValues={autocompleteDisplayValues}
|
||||
filterField={filterField}
|
||||
setDisplayValues={setAutocompleteDisplayValues}
|
||||
onFilterPropertyChange={onFilterPropertyChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</form>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
FilterContent.displayName = "FilterContent";
|
||||
export default FilterContent;
|
158
src/components/Filter/FilterContent/FilterContent.tsx
Normal file
158
src/components/Filter/FilterContent/FilterContent.tsx
Normal file
|
@ -0,0 +1,158 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import CollectionWithDividers from "@saleor/components/CollectionWithDividers";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import useStateFromProps from "@saleor/hooks/useStateFromProps";
|
||||
import React from "react";
|
||||
|
||||
import { FilterAutocompleteDisplayValues } from "../FilterAutocompleteField";
|
||||
import { FilterReducerAction } from "../reducer";
|
||||
import {
|
||||
FieldType,
|
||||
FilterErrorMessages,
|
||||
FilterErrors,
|
||||
IFilter,
|
||||
IFilterElement
|
||||
} from "../types";
|
||||
import FilterContentBody, { FilterContentBodyProps } from "./FilterContentBody";
|
||||
import FilterContentBodyNameField from "./FilterContentBodyNameField";
|
||||
import FilterContentHeader from "./FilterContentHeader";
|
||||
import FilterErrorsList from "./FilterErrorsList";
|
||||
|
||||
export interface FilterContentProps<T extends string = string> {
|
||||
filters: IFilter<T>;
|
||||
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||
onClear: () => void;
|
||||
onSubmit: () => void;
|
||||
currencySymbol?: string;
|
||||
dataStructure: IFilter<T>;
|
||||
errors?: FilterErrors;
|
||||
errorMessages?: FilterErrorMessages<T>;
|
||||
}
|
||||
|
||||
const FilterContent: React.FC<FilterContentProps> = ({
|
||||
currencySymbol,
|
||||
errors,
|
||||
errorMessages,
|
||||
filters,
|
||||
onClear,
|
||||
onFilterPropertyChange,
|
||||
onSubmit,
|
||||
dataStructure
|
||||
}) => {
|
||||
const getAutocompleteValuesWithNewValues = (
|
||||
autocompleteDisplayValues: FilterAutocompleteDisplayValues,
|
||||
filterField: IFilterElement<string>
|
||||
) => {
|
||||
if (filterField.type === FieldType.autocomplete) {
|
||||
return {
|
||||
...autocompleteDisplayValues,
|
||||
[filterField.name]: filterField.options
|
||||
};
|
||||
}
|
||||
|
||||
return autocompleteDisplayValues;
|
||||
};
|
||||
|
||||
const initialAutocompleteDisplayValues = filters.reduce(
|
||||
(acc, filterField) => {
|
||||
if (filterField.multipleFields) {
|
||||
return filterField.multipleFields.reduce(
|
||||
getAutocompleteValuesWithNewValues,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
return getAutocompleteValuesWithNewValues(acc, filterField);
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const [
|
||||
autocompleteDisplayValues,
|
||||
setAutocompleteDisplayValues
|
||||
] = useStateFromProps<FilterAutocompleteDisplayValues>(
|
||||
initialAutocompleteDisplayValues
|
||||
);
|
||||
|
||||
const commonFilterBodyProps: Omit<
|
||||
FilterContentBodyProps,
|
||||
"filter" | "onFilterPropertyChange"
|
||||
> = {
|
||||
currencySymbol,
|
||||
autocompleteDisplayValues,
|
||||
setAutocompleteDisplayValues,
|
||||
initialAutocompleteDisplayValues
|
||||
};
|
||||
|
||||
const handleMultipleFieldPropertyChange = function<T extends string>(
|
||||
action: FilterReducerAction<T>
|
||||
) {
|
||||
const { update } = action.payload;
|
||||
|
||||
onFilterPropertyChange({
|
||||
...action,
|
||||
payload: { ...action.payload, update: { ...update, active: true } }
|
||||
});
|
||||
};
|
||||
|
||||
const getFilterFromCurrentData = function<T extends string>(
|
||||
filter: IFilterElement<T>
|
||||
) {
|
||||
return filters.find(({ name }) => filter.name === name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
<form
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<FilterContentHeader onClear={onClear} />
|
||||
<Hr />
|
||||
{dataStructure
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
.map(filter => (
|
||||
<React.Fragment key={filter.name}>
|
||||
<FilterContentBodyNameField
|
||||
filter={getFilterFromCurrentData(filter)}
|
||||
onFilterPropertyChange={onFilterPropertyChange}
|
||||
/>
|
||||
<FilterErrorsList
|
||||
errors={errors}
|
||||
errorMessages={errorMessages}
|
||||
filter={filter}
|
||||
/>
|
||||
{filter.multipleFields ? (
|
||||
<CollectionWithDividers
|
||||
collection={filter.multipleFields}
|
||||
renderItem={filterField => (
|
||||
<FilterContentBody
|
||||
{...commonFilterBodyProps}
|
||||
onFilterPropertyChange={handleMultipleFieldPropertyChange}
|
||||
filter={{
|
||||
...getFilterFromCurrentData(filterField),
|
||||
active: getFilterFromCurrentData(filter).active
|
||||
}}
|
||||
>
|
||||
<Typography>{filterField.label}</Typography>
|
||||
</FilterContentBody>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<FilterContentBody
|
||||
{...commonFilterBodyProps}
|
||||
onFilterPropertyChange={onFilterPropertyChange}
|
||||
filter={getFilterFromCurrentData(filter)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</form>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
FilterContent.displayName = "FilterContent";
|
||||
export default FilterContent;
|
298
src/components/Filter/FilterContent/FilterContentBody.tsx
Normal file
298
src/components/Filter/FilterContent/FilterContentBody.tsx
Normal file
|
@ -0,0 +1,298 @@
|
|||
import {
|
||||
fade,
|
||||
FormControlLabel,
|
||||
makeStyles,
|
||||
Radio,
|
||||
TextField
|
||||
} from "@material-ui/core";
|
||||
import FormSpacer from "@saleor/components/FormSpacer";
|
||||
import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField";
|
||||
import SingleSelectField from "@saleor/components/SingleSelectField";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import Arrow from "../Arrow";
|
||||
import FilterAutocompleteField, {
|
||||
FilterAutocompleteDisplayValues
|
||||
} from "../FilterAutocompleteField";
|
||||
import FilterOptionField from "../FilterOptionField";
|
||||
import { FilterReducerAction } from "../reducer";
|
||||
import { FieldType, FilterType, IFilterElement } from "../types";
|
||||
import { getIsFilterMultipleChoices } from "./utils";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
andLabel: {
|
||||
margin: theme.spacing(0, 2)
|
||||
},
|
||||
arrow: {
|
||||
marginRight: theme.spacing(2)
|
||||
},
|
||||
filterSettings: {
|
||||
background: fade(theme.palette.primary.main, 0.1),
|
||||
padding: theme.spacing(2, 3)
|
||||
},
|
||||
input: {
|
||||
padding: "12px 0 9px 12px"
|
||||
},
|
||||
inputRange: {
|
||||
alignItems: "center",
|
||||
display: "flex"
|
||||
},
|
||||
|
||||
option: {
|
||||
left: -theme.spacing(0.5),
|
||||
position: "relative"
|
||||
},
|
||||
optionRadio: {
|
||||
left: -theme.spacing(0.25)
|
||||
}
|
||||
}),
|
||||
{ name: "FilterContentBody" }
|
||||
);
|
||||
|
||||
const filterTestingContext = "filter-field";
|
||||
|
||||
export interface FilterContentBodyProps<T extends string = string> {
|
||||
children?: React.ReactNode;
|
||||
filter: IFilterElement<T>;
|
||||
currencySymbol?: string;
|
||||
initialAutocompleteDisplayValues: FilterAutocompleteDisplayValues;
|
||||
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||
autocompleteDisplayValues: FilterAutocompleteDisplayValues;
|
||||
setAutocompleteDisplayValues: React.Dispatch<
|
||||
React.SetStateAction<Record<string, MultiAutocompleteChoiceType[]>>
|
||||
>;
|
||||
}
|
||||
|
||||
const FilterContentBody: React.FC<FilterContentBodyProps> = ({
|
||||
filter,
|
||||
children,
|
||||
currencySymbol,
|
||||
onFilterPropertyChange,
|
||||
autocompleteDisplayValues,
|
||||
setAutocompleteDisplayValues,
|
||||
initialAutocompleteDisplayValues
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
|
||||
if (!filter?.active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.filterSettings}>
|
||||
{children}
|
||||
{filter.type === FieldType.text && (
|
||||
<TextField
|
||||
data-test={filterTestingContext}
|
||||
data-test-id={filter.name}
|
||||
fullWidth
|
||||
name={filter.name}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
value={filter.value[0]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filter.name,
|
||||
update: {
|
||||
value: [event.target.value, filter.value[1]]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{[FieldType.date, FieldType.price, FieldType.number].includes(
|
||||
filter.type
|
||||
) && (
|
||||
<>
|
||||
<SingleSelectField
|
||||
data-test="filterRangeTypeChoice"
|
||||
choices={getIsFilterMultipleChoices(intl)}
|
||||
value={filter.multiple ? FilterType.MULTIPLE : FilterType.SINGULAR}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
}
|
||||
}}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filter.name,
|
||||
update: {
|
||||
multiple: event.target.value === FilterType.MULTIPLE
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<div className={classes.inputRange}>
|
||||
<div>
|
||||
<Arrow className={classes.arrow} />
|
||||
</div>
|
||||
{filter.multiple ? (
|
||||
<>
|
||||
<TextField
|
||||
data-test={filterTestingContext}
|
||||
data-test-id={filter.name}
|
||||
data-test-range-type="min"
|
||||
fullWidth
|
||||
name={filter.name + "_min"}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
endAdornment:
|
||||
filter.type === FieldType.price && currencySymbol,
|
||||
type: filter.type === FieldType.date ? "date" : "number"
|
||||
}}
|
||||
value={filter.value[0]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filter.name,
|
||||
update: {
|
||||
value: [event.target.value, filter.value[1]]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className={classes.andLabel}>
|
||||
<FormattedMessage
|
||||
defaultMessage="and"
|
||||
description="filter range separator"
|
||||
/>
|
||||
</span>
|
||||
<TextField
|
||||
data-test={filterTestingContext}
|
||||
data-test-id={filter.name}
|
||||
data-test-range-type="max"
|
||||
fullWidth
|
||||
name={filter.name + "_max"}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
endAdornment:
|
||||
filter.type === FieldType.price && currencySymbol,
|
||||
type: filter.type === FieldType.date ? "date" : "number"
|
||||
}}
|
||||
value={filter.value[1]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filter.name,
|
||||
update: {
|
||||
value: [filter.value[0], event.target.value]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<TextField
|
||||
data-test={filterTestingContext}
|
||||
data-test-id={filter.name}
|
||||
fullWidth
|
||||
name={filter.name}
|
||||
InputProps={{
|
||||
classes: {
|
||||
input: classes.input
|
||||
},
|
||||
endAdornment:
|
||||
filter.type === FieldType.price && currencySymbol,
|
||||
type:
|
||||
filter.type === FieldType.date
|
||||
? "date"
|
||||
: [FieldType.number, FieldType.price].includes(
|
||||
filter.type
|
||||
)
|
||||
? "number"
|
||||
: "text"
|
||||
}}
|
||||
value={filter.value[0]}
|
||||
onChange={event =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filter.name,
|
||||
update: {
|
||||
value: [event.target.value, filter.value[1]]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{filter.type === FieldType.options && (
|
||||
<FilterOptionField
|
||||
data-test={filterTestingContext}
|
||||
data-test-id={filter.name}
|
||||
filterField={filter}
|
||||
onFilterPropertyChange={onFilterPropertyChange}
|
||||
/>
|
||||
)}
|
||||
{filter.type === FieldType.boolean &&
|
||||
filter.options.map(option => (
|
||||
<div
|
||||
className={classNames(classes.option, classes.optionRadio)}
|
||||
key={option.value}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Radio
|
||||
data-test="filterBoolean"
|
||||
data-test-is-checked={filter.value[0] === option.value}
|
||||
checked={filter.value[0] === option.value}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={option.label}
|
||||
name={filter.name}
|
||||
onChange={() =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filter.name,
|
||||
update: {
|
||||
value: [option.value]
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{filter.type === FieldType.autocomplete && filter.multiple && (
|
||||
<FilterAutocompleteField
|
||||
data-test={filterTestingContext}
|
||||
data-test-id={filter.name}
|
||||
displayValues={autocompleteDisplayValues}
|
||||
filterField={filter}
|
||||
setDisplayValues={setAutocompleteDisplayValues}
|
||||
onFilterPropertyChange={onFilterPropertyChange}
|
||||
initialDisplayValues={initialAutocompleteDisplayValues}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterContentBody;
|
|
@ -0,0 +1,61 @@
|
|||
import { Checkbox, FormControlLabel, makeStyles } from "@material-ui/core";
|
||||
import React from "react";
|
||||
|
||||
import { FilterReducerAction } from "../reducer";
|
||||
import { IFilterElement } from "../types";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
container: {
|
||||
"&:not(:last-of-type)": {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`
|
||||
},
|
||||
padding: theme.spacing(1, 2.5)
|
||||
}
|
||||
}),
|
||||
{ name: "FilterContentBodyNameField" }
|
||||
);
|
||||
|
||||
export interface FilterContentBodyNameFieldProps<T extends string = string> {
|
||||
filter: IFilterElement<T>;
|
||||
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||
}
|
||||
|
||||
const FilterContentBodyNameField: React.FC<FilterContentBodyNameFieldProps> = ({
|
||||
filter,
|
||||
onFilterPropertyChange
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
|
||||
if (!filter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
data-test="filterGroupActive"
|
||||
data-test-id={filter.name}
|
||||
checked={filter.active}
|
||||
/>
|
||||
}
|
||||
label={filter.label}
|
||||
onChange={() =>
|
||||
onFilterPropertyChange({
|
||||
payload: {
|
||||
name: filter.name,
|
||||
update: {
|
||||
active: !filter.active
|
||||
}
|
||||
},
|
||||
type: "set-property"
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterContentBodyNameField;
|
55
src/components/Filter/FilterContent/FilterContentHeader.tsx
Normal file
55
src/components/Filter/FilterContent/FilterContentHeader.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Button, makeStyles, Typography } from "@material-ui/core";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
container: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: theme.spacing(1, 3)
|
||||
},
|
||||
clear: {
|
||||
marginRight: theme.spacing(1)
|
||||
},
|
||||
label: {
|
||||
fontWeight: 600
|
||||
}
|
||||
}),
|
||||
{ name: "FilterContentHeader" }
|
||||
);
|
||||
|
||||
interface FilterContentHeaderProps {
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
const FilterContentHeader: React.FC<FilterContentHeaderProps> = ({
|
||||
onClear
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<Typography className={classes.label}>
|
||||
<FormattedMessage defaultMessage="Filters" />
|
||||
</Typography>
|
||||
<div>
|
||||
<Button data-test="clear" className={classes.clear} onClick={onClear}>
|
||||
<FormattedMessage {...buttonMessages.clear} />
|
||||
</Button>
|
||||
<Button
|
||||
data-test="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
type="submit"
|
||||
>
|
||||
<FormattedMessage {...buttonMessages.done} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterContentHeader;
|
80
src/components/Filter/FilterContent/FilterErrorsList.tsx
Normal file
80
src/components/Filter/FilterContent/FilterErrorsList.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { fade, makeStyles } from "@material-ui/core/styles";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import InlineAlert from "@saleor/components/Alert/InlineAlert";
|
||||
import { useStyles as useDotStyles } from "@saleor/components/StatusLabel";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { FilterErrorMessages, FilterErrors, IFilterElement } from "../types";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
container: {
|
||||
backgroundColor: fade(theme.palette.primary.main, 0.1),
|
||||
padding: theme.spacing(3, 3, 0, 3)
|
||||
},
|
||||
listItemTitle: {
|
||||
color: theme.palette.primary.contrastText
|
||||
},
|
||||
dot: {
|
||||
backgroundColor: theme.palette.primary.contrastText,
|
||||
marginRight: theme.spacing(1)
|
||||
},
|
||||
itemContainer: {
|
||||
display: "flex",
|
||||
alignItems: "center"
|
||||
}
|
||||
}),
|
||||
{ name: "FilterErrorsList" }
|
||||
);
|
||||
|
||||
interface FilterErrorsListProps<T extends string = string> {
|
||||
filter: IFilterElement<T>;
|
||||
errors?: FilterErrors;
|
||||
errorMessages?: FilterErrorMessages<T>;
|
||||
}
|
||||
|
||||
const FilterErrorsList: React.FC<FilterErrorsListProps> = ({
|
||||
filter: { name, multipleFields },
|
||||
errors = [],
|
||||
errorMessages
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
const dotClasses = useDotStyles({});
|
||||
const intl = useIntl();
|
||||
|
||||
const hasError = (fieldName: string) =>
|
||||
!!errors.find(errorName => errorName === fieldName);
|
||||
|
||||
const hasErrorsToShow = () => {
|
||||
if (!!multipleFields?.length) {
|
||||
return multipleFields.some(multipleField => hasError(multipleField.name));
|
||||
}
|
||||
|
||||
return hasError(name);
|
||||
};
|
||||
|
||||
if (!errors.length || !hasErrorsToShow()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
{!!errors.length && (
|
||||
<InlineAlert>
|
||||
{errors.map(fieldName => (
|
||||
<div className={classes.itemContainer}>
|
||||
<div className={classNames(classes.dot, dotClasses.dot)} />
|
||||
<Typography className={classes.listItemTitle}>
|
||||
{intl.formatMessage(errorMessages?.[fieldName])}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</InlineAlert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterErrorsList;
|
2
src/components/Filter/FilterContent/index.tsx
Normal file
2
src/components/Filter/FilterContent/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./FilterContent";
|
||||
export { default } from "./FilterContent";
|
27
src/components/Filter/FilterContent/utils.ts
Normal file
27
src/components/Filter/FilterContent/utils.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import { FilterType } from "../types";
|
||||
|
||||
export function getIsFilterMultipleChoices(
|
||||
intl: IntlShape
|
||||
): SingleAutocompleteChoiceType[] {
|
||||
return [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "equal to",
|
||||
description: "is filter range or value",
|
||||
id: "is filter range equal to value"
|
||||
}),
|
||||
value: FilterType.SINGULAR
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "between",
|
||||
description: "is filter range or value",
|
||||
id: "is filter range between value"
|
||||
}),
|
||||
value: FilterType.MULTIPLE
|
||||
}
|
||||
];
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { FetchMoreProps, SearchPageProps } from "@saleor/types";
|
||||
import { MessageDescriptor } from "react-intl";
|
||||
|
||||
import { MultiAutocompleteChoiceType } from "../MultiAutocompleteSelectField";
|
||||
import { FilterReducerAction } from "./reducer";
|
||||
|
@ -28,7 +29,9 @@ export interface IFilterElement<T extends string = string>
|
|||
group?: T;
|
||||
label: string;
|
||||
name: T;
|
||||
type: FieldType;
|
||||
type?: FieldType;
|
||||
required?: boolean;
|
||||
multipleFields?: IFilterElement[];
|
||||
}
|
||||
|
||||
export interface FilterBaseFieldProps<T extends string = string> {
|
||||
|
@ -36,6 +39,13 @@ export interface FilterBaseFieldProps<T extends string = string> {
|
|||
onFilterPropertyChange: React.Dispatch<FilterReducerAction<T>>;
|
||||
}
|
||||
|
||||
export type FilterErrors = string[];
|
||||
|
||||
export type FilterErrorMessages<T extends string> = Record<
|
||||
T,
|
||||
MessageDescriptor
|
||||
>;
|
||||
|
||||
export type IFilter<T extends string = string> = Array<IFilterElement<T>>;
|
||||
|
||||
export enum FilterType {
|
||||
|
|
|
@ -9,15 +9,31 @@ export type UseFilter<T extends string> = [
|
|||
() => void
|
||||
];
|
||||
|
||||
function getParsedInitialFilter<T extends string>(
|
||||
initialFilter: IFilter<T>
|
||||
): IFilter<T> {
|
||||
return initialFilter.reduce((resultFilter, filterField) => {
|
||||
if (filterField.multipleFields) {
|
||||
return resultFilter
|
||||
.concat(filterField.multipleFields)
|
||||
.concat([filterField]);
|
||||
}
|
||||
|
||||
return resultFilter.concat(filterField);
|
||||
}, []);
|
||||
}
|
||||
|
||||
function useFilter<T extends string>(initialFilter: IFilter<T>): UseFilter<T> {
|
||||
const parsedInitialFilter = getParsedInitialFilter(initialFilter);
|
||||
|
||||
const [data, dispatchFilterAction] = useReducer<
|
||||
React.Reducer<IFilter<T>, FilterReducerAction<T>>
|
||||
>(reduceFilter, initialFilter);
|
||||
>(reduceFilter, parsedInitialFilter);
|
||||
|
||||
const reset = () =>
|
||||
dispatchFilterAction({
|
||||
payload: {
|
||||
new: initialFilter.map(each => ({
|
||||
new: parsedInitialFilter.map(each => ({
|
||||
...each,
|
||||
active: false,
|
||||
value: []
|
||||
|
@ -29,7 +45,7 @@ function useFilter<T extends string>(initialFilter: IFilter<T>): UseFilter<T> {
|
|||
const refresh = () =>
|
||||
dispatchFilterAction({
|
||||
payload: {
|
||||
new: initialFilter
|
||||
new: parsedInitialFilter
|
||||
},
|
||||
type: "merge"
|
||||
});
|
||||
|
|
68
src/components/Filter/utils.ts
Normal file
68
src/components/Filter/utils.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import compact from "lodash-es/compact";
|
||||
|
||||
import { FieldType, IFilterElement } from "./types";
|
||||
|
||||
export const getByName = (nameToCompare: string) => (obj: { name: string }) =>
|
||||
obj.name === nameToCompare;
|
||||
|
||||
export const isAutocompleteFilterFieldValid = function<T extends string>({
|
||||
value
|
||||
}: IFilterElement<T>) {
|
||||
return !!compact(value).length;
|
||||
};
|
||||
|
||||
export const isFilterFieldValid = function<T extends string>(
|
||||
filter: IFilterElement<T>
|
||||
) {
|
||||
const { type } = filter;
|
||||
|
||||
switch (type) {
|
||||
case FieldType.boolean:
|
||||
case FieldType.autocomplete:
|
||||
return isAutocompleteFilterFieldValid(filter);
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export const isFilterValid = function<T extends string>(
|
||||
resultFilters: Array<IFilterElement<T>>,
|
||||
filter: IFilterElement<T>
|
||||
) {
|
||||
const { required, active } = filter;
|
||||
|
||||
if (!required || !active) {
|
||||
return resultFilters;
|
||||
}
|
||||
|
||||
return isFilterFieldValid(filter)
|
||||
? resultFilters
|
||||
: [...resultFilters, filter];
|
||||
};
|
||||
|
||||
export const extractInvalidFilters = function<T extends string>(
|
||||
filtersData: Array<IFilterElement<T>>,
|
||||
filtersDataStructure: Array<IFilterElement<T>>
|
||||
) {
|
||||
return filtersDataStructure.reduce(
|
||||
(resultFilters, { name, multipleFields }) => {
|
||||
const filter = filtersData.find(getByName(name));
|
||||
|
||||
const shouldExtractChildrenFields =
|
||||
filter.active && !!multipleFields?.length;
|
||||
|
||||
if (shouldExtractChildrenFields) {
|
||||
return multipleFields
|
||||
.map(field => {
|
||||
const dataField = filtersData.find(getByName(field.name));
|
||||
return { ...dataField, active: true };
|
||||
})
|
||||
.reduce(isFilterValid, resultFilters);
|
||||
}
|
||||
|
||||
return isFilterValid(resultFilters, filter);
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@ import { FormattedMessage, useIntl } from "react-intl";
|
|||
|
||||
import { FilterProps } from "../../types";
|
||||
import Filter from "../Filter";
|
||||
import { IFilter } from "../Filter/types";
|
||||
import { FilterErrorMessages, IFilter } from "../Filter/types";
|
||||
import { SearchBarProps } from "../SearchBar";
|
||||
import SearchInput from "../SearchBar/SearchInput";
|
||||
import FilterTabs, { FilterTab } from "../TableFilter";
|
||||
|
@ -13,6 +13,7 @@ import FilterTabs, { FilterTab } from "../TableFilter";
|
|||
export interface FilterBarProps<TKeys extends string = string>
|
||||
extends FilterProps<TKeys>,
|
||||
SearchBarProps {
|
||||
errorMessages?: FilterErrorMessages<TKeys>;
|
||||
filterStructure: IFilter<TKeys>;
|
||||
}
|
||||
|
||||
|
@ -49,7 +50,8 @@ const FilterBar: React.FC<FilterBarProps> = props => {
|
|||
onFilterChange,
|
||||
onTabChange,
|
||||
onTabDelete,
|
||||
onTabSave
|
||||
onTabSave,
|
||||
errorMessages
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
|
@ -84,6 +86,7 @@ const FilterBar: React.FC<FilterBarProps> = props => {
|
|||
</FilterTabs>
|
||||
<div className={classes.root}>
|
||||
<Filter
|
||||
errorMessages={errorMessages}
|
||||
menu={filterStructure}
|
||||
currencySymbol={currencySymbol}
|
||||
onFilterAdd={onFilterChange}
|
||||
|
|
|
@ -1,43 +1,55 @@
|
|||
import grey from "@material-ui/core/colors/grey";
|
||||
import yellow from "@material-ui/core/colors/yellow";
|
||||
import Typography, { TypographyProps } from "@material-ui/core/Typography";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Label from "@saleor/orders/components/OrderHistory/Label";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
export const useStyles = makeStyles(
|
||||
theme => {
|
||||
const dot = {
|
||||
borderRadius: "100%",
|
||||
content: "''",
|
||||
display: "block",
|
||||
height: 8,
|
||||
left: -theme.spacing(2),
|
||||
position: "absolute" as "absolute",
|
||||
top: "calc(50% - 5px)",
|
||||
width: 8
|
||||
};
|
||||
|
||||
return {
|
||||
dot,
|
||||
container: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
},
|
||||
containerVertical: {
|
||||
alignItems: "flex-start"
|
||||
},
|
||||
textContainer: {
|
||||
marginLeft: theme.spacing(1),
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
},
|
||||
dotVertical: {
|
||||
marginTop: theme.spacing(1)
|
||||
},
|
||||
alertDot: {
|
||||
"&:before": { backgroundColor: yellow[500], ...dot }
|
||||
backgroundColor: yellow[500],
|
||||
...dot
|
||||
},
|
||||
errorDot: {
|
||||
"&:before": { backgroundColor: theme.palette.error.main, ...dot }
|
||||
backgroundColor: theme.palette.error.main,
|
||||
...dot
|
||||
},
|
||||
neutralDot: {
|
||||
"&:before": { backgroundColor: grey[300], ...dot }
|
||||
backgroundColor: grey[300],
|
||||
...dot
|
||||
},
|
||||
root: {
|
||||
display: "inline-block",
|
||||
marginLeft: theme.spacing(1) + 8,
|
||||
position: "relative"
|
||||
successDot: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
...dot
|
||||
},
|
||||
span: {
|
||||
display: "inline"
|
||||
},
|
||||
successDot: {
|
||||
"&:before": { backgroundColor: theme.palette.primary.main, ...dot }
|
||||
}
|
||||
};
|
||||
},
|
||||
|
@ -45,41 +57,43 @@ const useStyles = makeStyles(
|
|||
);
|
||||
|
||||
interface StatusLabelProps {
|
||||
className?: string;
|
||||
label: string | React.ReactNode;
|
||||
status: "success" | "alert" | "neutral" | "error" | string;
|
||||
typographyProps?: TypographyProps;
|
||||
subtitle?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const StatusLabel: React.FC<StatusLabelProps> = props => {
|
||||
const { className, label, status, typographyProps } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const StatusLabel: React.FC<StatusLabelProps> = ({
|
||||
className,
|
||||
label,
|
||||
status,
|
||||
subtitle
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
[classes.root]: true,
|
||||
[className]: true,
|
||||
[classes.successDot]: status === "success",
|
||||
[classes.alertDot]: status === "alert",
|
||||
[classes.neutralDot]: status === "neutral",
|
||||
[classes.errorDot]: status === "error"
|
||||
[classes.container]: true,
|
||||
[classes.containerVertical]: !!subtitle
|
||||
})}
|
||||
>
|
||||
{typographyProps ? (
|
||||
<Typography
|
||||
component="span"
|
||||
className={classes.span}
|
||||
{...typographyProps}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
<div
|
||||
className={classNames({
|
||||
[className]: true,
|
||||
[classes.dotVertical]: !!subtitle,
|
||||
[classes.successDot]: status === "success",
|
||||
[classes.alertDot]: status === "alert",
|
||||
[classes.neutralDot]: status === "neutral",
|
||||
[classes.errorDot]: status === "error"
|
||||
})}
|
||||
></div>
|
||||
<div className={classes.textContainer}>
|
||||
<Typography>{label}</Typography>
|
||||
{subtitle && <Label text={subtitle} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
StatusLabel.displayName = "StatusLabel";
|
||||
|
||||
export default StatusLabel;
|
||||
|
|
16
src/components/StatusLabel/messages.ts
Normal file
16
src/components/StatusLabel/messages.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const statusLabelMessages = defineMessages({
|
||||
active: {
|
||||
defaultMessage: "Active",
|
||||
description: "status label active"
|
||||
},
|
||||
inactive: {
|
||||
defaultMessage: "Inactive",
|
||||
description: "status label inactive"
|
||||
},
|
||||
deactivated: {
|
||||
defaultMessage: "Deactivated",
|
||||
description: "status label deactivated"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import CentralPlacementDecorator from "@saleor/storybook/CentralPlacementDecorator";
|
||||
import CommonDecorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import * as messages from "../../pageTypes/hooks/usePageTypeDelete/messages";
|
||||
import TypeDeleteWarningDialog, {
|
||||
TypeBaseData,
|
||||
TypeDeleteWarningDialogProps
|
||||
} from "./TypeDeleteWarningDialog";
|
||||
|
||||
const props: TypeDeleteWarningDialogProps<TypeBaseData> = {
|
||||
...messages,
|
||||
isOpen: true,
|
||||
onClose: () => undefined,
|
||||
onDelete: () => undefined,
|
||||
typesData: [{ id: "id-1", name: "Interesting Pages" }],
|
||||
isLoading: false,
|
||||
assignedItemsCount: 4,
|
||||
typesToDelete: ["id-1"],
|
||||
viewAssignedItemsUrl: "some-url",
|
||||
deleteButtonState: "default"
|
||||
};
|
||||
|
||||
storiesOf("TypeDeleteWarningDialog.stories", module)
|
||||
.addDecorator(CommonDecorator)
|
||||
.addDecorator(CentralPlacementDecorator)
|
||||
.add("loading", () => <TypeDeleteWarningDialog {...props} isLoading={true} />)
|
||||
.add("single type no assigned items", () => (
|
||||
<TypeDeleteWarningDialog {...props} assignedItemsCount={0} />
|
||||
))
|
||||
.add("single type some assigned items", () => (
|
||||
<TypeDeleteWarningDialog {...props} />
|
||||
))
|
||||
.add("multiple type no assigned items", () => (
|
||||
<TypeDeleteWarningDialog {...props} assignedItemsCount={0} />
|
||||
))
|
||||
.add("multiple types some assigned items", () => (
|
||||
<TypeDeleteWarningDialog {...props} typesToDelete={["id-1", "id-2"]} />
|
||||
));
|
|
@ -0,0 +1,132 @@
|
|||
import { CardContent } from "@material-ui/core";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
import Modal from "@material-ui/core/Modal";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
|
||||
import ModalTitle from "@saleor/orders/components/OrderDiscountCommonModal/ModalTitle";
|
||||
import { getById } from "@saleor/orders/components/OrderReturnPage/utils";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { useTypeDeleteWarningDialogStyles as useStyles } from "./styles";
|
||||
import ProductTypeDeleteWarningDialogContent from "./TypeDeleteWarningDialogContent";
|
||||
import {
|
||||
CommonTypeDeleteWarningMessages,
|
||||
TypeDeleteWarningMessages
|
||||
} from "./types";
|
||||
|
||||
export interface TypeBaseData {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TypeDeleteMessages {
|
||||
baseMessages: CommonTypeDeleteWarningMessages;
|
||||
singleWithItemsMessages: TypeDeleteWarningMessages;
|
||||
singleWithoutItemsMessages: TypeDeleteWarningMessages;
|
||||
multipleWithItemsMessages: TypeDeleteWarningMessages;
|
||||
multipleWithoutItemsMessages: TypeDeleteWarningMessages;
|
||||
}
|
||||
|
||||
export interface TypeDeleteWarningDialogProps<T extends TypeBaseData>
|
||||
extends TypeDeleteMessages {
|
||||
isOpen: boolean;
|
||||
deleteButtonState: ConfirmButtonTransitionState;
|
||||
onClose: () => void;
|
||||
onDelete: () => void;
|
||||
viewAssignedItemsUrl: string;
|
||||
typesToDelete: string[];
|
||||
assignedItemsCount: number | undefined;
|
||||
isLoading?: boolean;
|
||||
typesData: T[];
|
||||
// temporary, until we add filters to pages list - SALEOR-3279
|
||||
showViewAssignedItemsButton?: boolean;
|
||||
}
|
||||
|
||||
function TypeDeleteWarningDialog<T extends TypeBaseData>({
|
||||
isLoading = false,
|
||||
isOpen,
|
||||
baseMessages,
|
||||
singleWithItemsMessages,
|
||||
singleWithoutItemsMessages,
|
||||
multipleWithItemsMessages,
|
||||
multipleWithoutItemsMessages,
|
||||
onClose,
|
||||
onDelete,
|
||||
assignedItemsCount,
|
||||
viewAssignedItemsUrl,
|
||||
typesToDelete,
|
||||
typesData,
|
||||
showViewAssignedItemsButton = true
|
||||
}: TypeDeleteWarningDialogProps<T>) {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
|
||||
const showMultiple = typesToDelete.length > 1;
|
||||
|
||||
const hasAssignedItems = !!assignedItemsCount;
|
||||
|
||||
const selectMessages = () => {
|
||||
if (showMultiple) {
|
||||
const multipleMessages = hasAssignedItems
|
||||
? multipleWithItemsMessages
|
||||
: multipleWithoutItemsMessages;
|
||||
|
||||
return {
|
||||
...multipleMessages
|
||||
};
|
||||
}
|
||||
|
||||
const singleMessages = hasAssignedItems
|
||||
? singleWithItemsMessages
|
||||
: singleWithoutItemsMessages;
|
||||
|
||||
return {
|
||||
...singleMessages
|
||||
};
|
||||
};
|
||||
|
||||
const { description, consentLabel } = selectMessages();
|
||||
|
||||
const singleItemSelectedId = typesToDelete[0];
|
||||
|
||||
const singleItemSelectedName = typesData.find(getById(singleItemSelectedId))
|
||||
?.name;
|
||||
|
||||
return (
|
||||
<Modal open={isOpen}>
|
||||
<div className={classes.centerContainer}>
|
||||
<Card className={classes.content}>
|
||||
<ModalTitle
|
||||
title={intl.formatMessage(baseMessages.title, {
|
||||
selectedTypesCount: typesToDelete.length
|
||||
})}
|
||||
withBorder
|
||||
onClose={onClose}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<CardContent className={classes.centerContainer}>
|
||||
<CircularProgress size={16} />
|
||||
</CardContent>
|
||||
) : (
|
||||
<ProductTypeDeleteWarningDialogContent
|
||||
showViewAssignedItemsButton={showViewAssignedItemsButton}
|
||||
assignedItemsCount={assignedItemsCount}
|
||||
hasAssignedItems={hasAssignedItems}
|
||||
singleItemSelectedName={singleItemSelectedName}
|
||||
viewAssignedItemsUrl={viewAssignedItemsUrl}
|
||||
onDelete={onDelete}
|
||||
description={description}
|
||||
consentLabel={consentLabel}
|
||||
viewAssignedItemsButtonLabel={
|
||||
baseMessages.viewAssignedItemsButtonLabel
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default TypeDeleteWarningDialog;
|
|
@ -0,0 +1,95 @@
|
|||
import CardContent from "@material-ui/core/CardContent";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import HorizontalSpacer from "@saleor/apps/components/HorizontalSpacer";
|
||||
import CardSpacer from "@saleor/components/CardSpacer";
|
||||
import ConfirmButton from "@saleor/components/ConfirmButton";
|
||||
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
|
||||
import DeleteButton from "@saleor/components/DeleteButton";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import React, { ChangeEvent, useState } from "react";
|
||||
import { MessageDescriptor, useIntl } from "react-intl";
|
||||
|
||||
import { useTypeDeleteWarningDialogStyles as useStyles } from "./styles";
|
||||
|
||||
interface TypeDeleteWarningDialogContentProps {
|
||||
singleItemSelectedName?: string;
|
||||
viewAssignedItemsButtonLabel: MessageDescriptor;
|
||||
description: MessageDescriptor;
|
||||
consentLabel: MessageDescriptor;
|
||||
viewAssignedItemsUrl: string;
|
||||
hasAssignedItems: boolean;
|
||||
assignedItemsCount: number | undefined;
|
||||
onDelete: () => void;
|
||||
// temporary, until we add filters to pages list - SALEOR-3279
|
||||
showViewAssignedItemsButton?: boolean;
|
||||
}
|
||||
|
||||
const TypeDeleteWarningDialogContent: React.FC<TypeDeleteWarningDialogContentProps> = ({
|
||||
description,
|
||||
consentLabel,
|
||||
viewAssignedItemsUrl,
|
||||
viewAssignedItemsButtonLabel,
|
||||
singleItemSelectedName,
|
||||
hasAssignedItems,
|
||||
assignedItemsCount,
|
||||
onDelete,
|
||||
showViewAssignedItemsButton
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigator();
|
||||
|
||||
const [isConsentChecked, setIsConsentChecked] = useState(false);
|
||||
|
||||
const handleConsentChange = ({ target }: ChangeEvent<any>) =>
|
||||
setIsConsentChecked(target.value);
|
||||
|
||||
const handleViewAssignedItems = () => navigate(viewAssignedItemsUrl);
|
||||
|
||||
const isDisbled = hasAssignedItems ? !isConsentChecked : false;
|
||||
|
||||
const shouldShowViewAssignedItemsButton =
|
||||
showViewAssignedItemsButton && hasAssignedItems;
|
||||
|
||||
return (
|
||||
<CardContent>
|
||||
<Typography>
|
||||
{intl.formatMessage(description, {
|
||||
typeName: singleItemSelectedName,
|
||||
assignedItemsCount,
|
||||
b: (...chunks) => <b>{chunks}</b>
|
||||
})}
|
||||
</Typography>
|
||||
<CardSpacer />
|
||||
{consentLabel && (
|
||||
<ControlledCheckbox
|
||||
name="delete-assigned-items-consent"
|
||||
checked={isConsentChecked}
|
||||
onChange={handleConsentChange}
|
||||
label={
|
||||
<Typography className={classes.consentLabel}>
|
||||
{intl.formatMessage(consentLabel)}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<CardSpacer />
|
||||
<div className={classes.buttonsSection}>
|
||||
{shouldShowViewAssignedItemsButton && (
|
||||
<>
|
||||
<ConfirmButton
|
||||
onClick={handleViewAssignedItems}
|
||||
transitionState="default"
|
||||
>
|
||||
{intl.formatMessage(viewAssignedItemsButtonLabel)}
|
||||
</ConfirmButton>
|
||||
<HorizontalSpacer spacing={3} />
|
||||
</>
|
||||
)}
|
||||
<DeleteButton onClick={onDelete} disabled={isDisbled} />
|
||||
</div>
|
||||
</CardContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default TypeDeleteWarningDialogContent;
|
2
src/components/TypeDeleteWarningDialog/index.tsx
Normal file
2
src/components/TypeDeleteWarningDialog/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./TypeDeleteWarningDialog";
|
||||
export { default } from "./TypeDeleteWarningDialog";
|
23
src/components/TypeDeleteWarningDialog/styles.ts
Normal file
23
src/components/TypeDeleteWarningDialog/styles.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { makeStyles } from "@saleor/theme";
|
||||
|
||||
export const useTypeDeleteWarningDialogStyles = makeStyles(
|
||||
theme => ({
|
||||
centerContainer: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%"
|
||||
},
|
||||
content: {
|
||||
width: 600
|
||||
},
|
||||
consentLabel: {
|
||||
color: theme.palette.primary.main
|
||||
},
|
||||
buttonsSection: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end"
|
||||
}
|
||||
}),
|
||||
{ name: "ProductTypeDeleteWarningDialog" }
|
||||
);
|
10
src/components/TypeDeleteWarningDialog/types.ts
Normal file
10
src/components/TypeDeleteWarningDialog/types.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { MessageDescriptor } from "react-intl";
|
||||
|
||||
export type CommonTypeDeleteWarningMessages = Record<
|
||||
"title" | "viewAssignedItemsButtonLabel",
|
||||
MessageDescriptor
|
||||
>;
|
||||
|
||||
export type TypeDeleteWarningMessages = Partial<
|
||||
Record<"description" | "consentLabel", MessageDescriptor>
|
||||
>;
|
|
@ -1,11 +1,42 @@
|
|||
import gql from "graphql-tag";
|
||||
|
||||
export const configurationItemFragment = gql`
|
||||
fragment ConfigurationItemFragment on ConfigurationItem {
|
||||
name
|
||||
value
|
||||
type
|
||||
helpText
|
||||
label
|
||||
}
|
||||
`;
|
||||
|
||||
export const pluginConfigurationFragment = gql`
|
||||
${configurationItemFragment}
|
||||
fragment PluginConfigurationFragment on PluginConfiguration {
|
||||
active
|
||||
channel {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
configuration {
|
||||
...ConfigurationItemFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const pluginsFragment = gql`
|
||||
${pluginConfigurationFragment}
|
||||
fragment PluginFragment on Plugin {
|
||||
id
|
||||
name
|
||||
description
|
||||
active
|
||||
globalConfiguration {
|
||||
...PluginConfigurationFragment
|
||||
}
|
||||
channelConfigurations {
|
||||
...PluginConfigurationFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -13,12 +44,5 @@ export const pluginsDetailsFragment = gql`
|
|||
${pluginsFragment}
|
||||
fragment PluginsDetailsFragment on Plugin {
|
||||
...PluginFragment
|
||||
configuration {
|
||||
name
|
||||
type
|
||||
value
|
||||
helpText
|
||||
label
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
19
src/fragments/types/ConfigurationItemFragment.ts
Normal file
19
src/fragments/types/ConfigurationItemFragment.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: ConfigurationItemFragment
|
||||
// ====================================================
|
||||
|
||||
export interface ConfigurationItemFragment {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
33
src/fragments/types/PluginConfiguarionFragment.ts
Normal file
33
src/fragments/types/PluginConfiguarionFragment.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: PluginConfiguarionFragment
|
||||
// ====================================================
|
||||
|
||||
export interface PluginConfiguarionFragment_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface PluginConfiguarionFragment_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface PluginConfiguarionFragment {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: PluginConfiguarionFragment_channel | null;
|
||||
configuration: (PluginConfiguarionFragment_configuration | null)[] | null;
|
||||
}
|
33
src/fragments/types/PluginConfigurationFragment.ts
Normal file
33
src/fragments/types/PluginConfigurationFragment.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: PluginConfigurationFragment
|
||||
// ====================================================
|
||||
|
||||
export interface PluginConfigurationFragment_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface PluginConfigurationFragment_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface PluginConfigurationFragment {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: PluginConfigurationFragment_channel | null;
|
||||
configuration: (PluginConfigurationFragment_configuration | null)[] | null;
|
||||
}
|
|
@ -3,14 +3,63 @@
|
|||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: PluginFragment
|
||||
// ====================================================
|
||||
|
||||
export interface PluginFragment_globalConfiguration_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface PluginFragment_globalConfiguration_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface PluginFragment_globalConfiguration {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: PluginFragment_globalConfiguration_channel | null;
|
||||
configuration: (PluginFragment_globalConfiguration_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface PluginFragment_channelConfigurations_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface PluginFragment_channelConfigurations_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface PluginFragment_channelConfigurations {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: PluginFragment_channelConfigurations_channel | null;
|
||||
configuration: (PluginFragment_channelConfigurations_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface PluginFragment {
|
||||
__typename: "Plugin";
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
globalConfiguration: PluginFragment_globalConfiguration | null;
|
||||
channelConfigurations: PluginFragment_channelConfigurations[];
|
||||
}
|
||||
|
|
|
@ -9,20 +9,57 @@ import { ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
|
|||
// GraphQL fragment: PluginsDetailsFragment
|
||||
// ====================================================
|
||||
|
||||
export interface PluginsDetailsFragment_configuration {
|
||||
export interface PluginsDetailsFragment_globalConfiguration_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface PluginsDetailsFragment_globalConfiguration_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface PluginsDetailsFragment_globalConfiguration {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: PluginsDetailsFragment_globalConfiguration_channel | null;
|
||||
configuration: (PluginsDetailsFragment_globalConfiguration_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface PluginsDetailsFragment_channelConfigurations_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface PluginsDetailsFragment_channelConfigurations_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface PluginsDetailsFragment_channelConfigurations {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: PluginsDetailsFragment_channelConfigurations_channel | null;
|
||||
configuration: (PluginsDetailsFragment_channelConfigurations_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface PluginsDetailsFragment {
|
||||
__typename: "Plugin";
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
configuration: (PluginsDetailsFragment_configuration | null)[] | null;
|
||||
globalConfiguration: PluginsDetailsFragment_globalConfiguration | null;
|
||||
channelConfigurations: PluginsDetailsFragment_channelConfigurations[];
|
||||
}
|
||||
|
|
21
src/hooks/useChannelsSearch.ts
Normal file
21
src/hooks/useChannelsSearch.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Channel_channel } from "@saleor/channels/types/Channel";
|
||||
import { FetchMoreProps, Search, SearchProps } from "@saleor/types";
|
||||
import { filter } from "fuzzaldrin";
|
||||
import React from "react";
|
||||
|
||||
export const useChannelsSearch = function<T extends { name: string }>(
|
||||
channels: T[]
|
||||
) {
|
||||
const [query, onQueryChange] = React.useState("");
|
||||
const filteredChannels =
|
||||
filter<T, "name">(channels, query, { key: "name" }) || [];
|
||||
|
||||
return { query, onQueryChange, filteredChannels };
|
||||
};
|
||||
|
||||
export interface ChannelsWithLoadMoreProps
|
||||
extends FetchMoreProps,
|
||||
Search,
|
||||
SearchProps {
|
||||
channels: Channel_channel[];
|
||||
}
|
66
src/hooks/useChannelsSearchWithLoadMore.ts
Normal file
66
src/hooks/useChannelsSearchWithLoadMore.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { useBaseChannelsList } from "@saleor/channels/queries";
|
||||
import chunk from "lodash-es/chunk";
|
||||
import compact from "lodash-es/compact";
|
||||
import concat from "lodash-es/concat";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
ChannelsWithLoadMoreProps,
|
||||
useChannelsSearch
|
||||
} from "./useChannelsSearch";
|
||||
|
||||
const DEFAULT_ITEMS_PER_PAGE = 6;
|
||||
const INITIAL_INDEX = 0;
|
||||
|
||||
export const useChannelsSearchWithLoadMore = (
|
||||
itemsPerPage: number = DEFAULT_ITEMS_PER_PAGE
|
||||
): ChannelsWithLoadMoreProps => {
|
||||
const { data, loading } = useBaseChannelsList({});
|
||||
|
||||
const {
|
||||
query,
|
||||
onQueryChange: onSearchChange,
|
||||
filteredChannels
|
||||
} = useChannelsSearch(data?.channels);
|
||||
|
||||
const allChannelsChunks = chunk(filteredChannels, itemsPerPage);
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState(INITIAL_INDEX);
|
||||
const [currentChannelsChunks, setCurrentChannelsChunks] = useState([]);
|
||||
|
||||
const handleAddInitialChunk = () => {
|
||||
if (data?.channels && !loading) {
|
||||
setCurrentChannelsChunks([allChannelsChunks[INITIAL_INDEX]]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(handleAddInitialChunk, [loading, query]);
|
||||
|
||||
const onFetchMore = () => {
|
||||
if (!hasMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = currentIndex + 1;
|
||||
setCurrentIndex(newIndex);
|
||||
|
||||
const newChunk = allChannelsChunks[newIndex];
|
||||
setCurrentChannelsChunks([...currentChannelsChunks, newChunk]);
|
||||
};
|
||||
|
||||
const hasMore = allChannelsChunks.length > currentChannelsChunks.length;
|
||||
|
||||
const channels = compact(concat([], ...currentChannelsChunks));
|
||||
|
||||
const totalCount = data?.channels.length;
|
||||
|
||||
return {
|
||||
query,
|
||||
onSearchChange,
|
||||
channels,
|
||||
hasMore,
|
||||
totalCount,
|
||||
onFetchMore,
|
||||
loading
|
||||
};
|
||||
};
|
|
@ -184,7 +184,12 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
|
|||
)
|
||||
) : (
|
||||
<>
|
||||
<Typography className={classes.userEmail}>{user.email}</Typography>
|
||||
<Typography
|
||||
className={classes.userEmail}
|
||||
data-test-id="customer-email"
|
||||
>
|
||||
{user.email}
|
||||
</Typography>
|
||||
<RequirePermissions
|
||||
userPermissions={userPermissions}
|
||||
requiredPermissions={[PermissionEnum.MANAGE_USERS]}
|
||||
|
@ -257,6 +262,7 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
|
|||
{canEditAddresses && (
|
||||
<div className={classes.sectionHeaderToolbar}>
|
||||
<Button
|
||||
data-test-id="edit-shipping-address"
|
||||
color="primary"
|
||||
variant="text"
|
||||
onClick={onShippingAddressEdit}
|
||||
|
@ -314,6 +320,7 @@ const OrderCustomer: React.FC<OrderCustomerProps> = props => {
|
|||
{canEditAddresses && (
|
||||
<div className={classes.sectionHeaderToolbar}>
|
||||
<Button
|
||||
data-test-id="edit-billing-address"
|
||||
color="primary"
|
||||
variant="text"
|
||||
onClick={onBillingAddressEdit}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import Divider from "@material-ui/core/Divider";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import CardSpacer from "@saleor/components/CardSpacer";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import React from "react";
|
||||
|
||||
|
@ -19,16 +21,29 @@ const useStyles = makeStyles(
|
|||
interface ModalTitleProps {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
withBorder?: boolean;
|
||||
}
|
||||
|
||||
const ModalTitle: React.FC<ModalTitleProps> = ({ title, onClose }) => {
|
||||
const ModalTitle: React.FC<ModalTitleProps> = ({
|
||||
title,
|
||||
onClose,
|
||||
withBorder = false
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<Typography variant="h5">{title}</Typography>
|
||||
<CloseIcon onClick={onClose} />
|
||||
</div>
|
||||
<>
|
||||
<div className={classes.container}>
|
||||
<Typography variant="h5">{title}</Typography>
|
||||
<CloseIcon onClick={onClose} />
|
||||
</div>
|
||||
{withBorder && (
|
||||
<>
|
||||
<CardSpacer />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
import DialogContentText from "@material-ui/core/DialogContentText";
|
||||
import ActionDialog from "@saleor/components/ActionDialog";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
export interface PageTypeDeleteDialogProps {
|
||||
confirmButtonState: ConfirmButtonTransitionState;
|
||||
open: boolean;
|
||||
name: string;
|
||||
hasPages: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const PageTypeDeleteDialog: React.FC<PageTypeDeleteDialogProps> = ({
|
||||
confirmButtonState,
|
||||
open,
|
||||
name,
|
||||
hasPages,
|
||||
onClose,
|
||||
onConfirm
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<ActionDialog
|
||||
confirmButtonState={confirmButtonState}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
title={intl.formatMessage({
|
||||
defaultMessage: "Delete Page Type",
|
||||
description: "dialog header"
|
||||
})}
|
||||
variant="delete"
|
||||
>
|
||||
<DialogContentText>
|
||||
{hasPages ? (
|
||||
<FormattedMessage
|
||||
defaultMessage="Page Type you want to delete is used by some pages. Deleting this page type will also delete those pages. Are you sure you want to delete {name}? After doing so you won’t be able to revert changes."
|
||||
description="delete page type with its pages"
|
||||
values={{
|
||||
name: <strong>{name}</strong>
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
defaultMessage="Are you sure you want to delete {name}? After doing so you won’t be able to revert changes."
|
||||
description="delete page type"
|
||||
values={{
|
||||
name: <strong>{name}</strong>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DialogContentText>
|
||||
</ActionDialog>
|
||||
);
|
||||
};
|
||||
PageTypeDeleteDialog.displayName = "PageTypeDeleteDialog";
|
||||
export default PageTypeDeleteDialog;
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from "./PageTypeDeleteDialog";
|
||||
export * from "./PageTypeDeleteDialog";
|
2
src/pageTypes/hooks/usePageTypeDelete/index.ts
Normal file
2
src/pageTypes/hooks/usePageTypeDelete/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./usePageTypeDelete";
|
||||
export { default } from "./usePageTypeDelete";
|
66
src/pageTypes/hooks/usePageTypeDelete/messages.ts
Normal file
66
src/pageTypes/hooks/usePageTypeDelete/messages.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const baseMessages = defineMessages({
|
||||
title: {
|
||||
defaultMessage:
|
||||
"Delete page {selectedTypesCount,plural,one{type} other{types}}",
|
||||
description: "PageTypeDeleteWarningDialog title"
|
||||
},
|
||||
viewAssignedItemsButtonLabel: {
|
||||
defaultMessage: "View pages",
|
||||
description:
|
||||
"PageTypeDeleteWarningDialog single assigned items button label"
|
||||
}
|
||||
});
|
||||
|
||||
export const singleWithItemsMessages = defineMessages({
|
||||
description: {
|
||||
defaultMessage:
|
||||
"You are about to delete page type <b>{typeName}</b>. It is assigned to {assignedItemsCount} {assignedItemsCount,plural,one{page} other{pages}}. Deleting this page type will also delete those pages. Are you sure you want to do this?",
|
||||
description:
|
||||
"PageTypeDeleteWarningDialog single assigned items description",
|
||||
id: "PageTypeDeleteWarningDialog single assigned items description"
|
||||
},
|
||||
consentLabel: {
|
||||
defaultMessage: "Yes, I want to delete this page type and assigned pages",
|
||||
description: "PageTypeDeleteWarningDialog single consent label",
|
||||
id: "PageTypeDeleteWarningDialog single consent label"
|
||||
}
|
||||
});
|
||||
|
||||
export const multipleWithItemsMessages = defineMessages({
|
||||
description: {
|
||||
defaultMessage:
|
||||
"You are about to delete multiple page types. Some of them are assigned to pages. Deleting those page types will also delete those pages",
|
||||
description: "PageTypeDeleteWarningDialog with items multiple description",
|
||||
id: "PageTypeDeleteWarningDialog with items multiple description"
|
||||
},
|
||||
consentLabel: {
|
||||
defaultMessage:
|
||||
"Yes, I want to delete those pages types and assigned pages",
|
||||
description: "PageTypeDeleteWarningDialog multiple consent label",
|
||||
id: "PageTypeDeleteWarningDialog multiple consent label"
|
||||
}
|
||||
});
|
||||
|
||||
export const singleWithoutItemsMessages = defineMessages({
|
||||
description: {
|
||||
defaultMessage:
|
||||
"Are you sure you want to delete <b>{typeName}</b>? If you remove it you won’t be able to assign it to created pages.",
|
||||
description:
|
||||
"PageTypeDeleteWarningDialog single no assigned items description",
|
||||
|
||||
id: "PageTypeDeleteWarningDialog single no assigned items description"
|
||||
}
|
||||
});
|
||||
|
||||
export const multipleWithoutItemsMessages = defineMessages({
|
||||
description: {
|
||||
defaultMessage:
|
||||
"Are you sure you want to delete selected page types? If you remove them you won’t be able to assign them to created pages.",
|
||||
description:
|
||||
"PageTypeDeleteWarningDialog multiple assigned items description",
|
||||
|
||||
id: "PageTypeDeleteWarningDialog multiple assigned items description"
|
||||
}
|
||||
});
|
16
src/pageTypes/hooks/usePageTypeDelete/types.ts
Normal file
16
src/pageTypes/hooks/usePageTypeDelete/types.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { TypeDeleteMessages } from "@saleor/components/TypeDeleteWarningDialog";
|
||||
import { Ids } from "@saleor/types";
|
||||
|
||||
export interface UseTypeDeleteData extends TypeDeleteMessages {
|
||||
isOpen: boolean;
|
||||
assignedItemsCount: number | undefined;
|
||||
viewAssignedItemsUrl: string;
|
||||
isLoading: boolean | undefined;
|
||||
typesToDelete: Ids;
|
||||
}
|
||||
|
||||
export interface UseTypeDeleteProps<T> {
|
||||
params: T;
|
||||
selectedTypes?: Ids;
|
||||
singleId?: string;
|
||||
}
|
64
src/pageTypes/hooks/usePageTypeDelete/usePageTypeDelete.tsx
Normal file
64
src/pageTypes/hooks/usePageTypeDelete/usePageTypeDelete.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { usePageCountQuery } from "@saleor/pages/queries";
|
||||
import { PageCountVariables } from "@saleor/pages/types/PageCount";
|
||||
import { pageListUrl } from "@saleor/pages/urls";
|
||||
import {
|
||||
PageTypeListUrlQueryParams,
|
||||
PageTypeUrlQueryParams
|
||||
} from "@saleor/pageTypes/urls";
|
||||
import React from "react";
|
||||
|
||||
import * as messages from "./messages";
|
||||
import { UseTypeDeleteData, UseTypeDeleteProps } from "./types";
|
||||
|
||||
type UsePageTypeDeleteProps<
|
||||
T = PageTypeListUrlQueryParams | PageTypeUrlQueryParams
|
||||
> = UseTypeDeleteProps<T>;
|
||||
|
||||
function usePageTypeDelete({
|
||||
singleId,
|
||||
params,
|
||||
selectedTypes
|
||||
}: UsePageTypeDeleteProps): UseTypeDeleteData {
|
||||
const pageTypes = selectedTypes || [singleId];
|
||||
|
||||
const isDeleteDialogOpen = params.action === "remove";
|
||||
|
||||
const pagesAssignedToSelectedTypesQueryVars = React.useMemo<
|
||||
PageCountVariables
|
||||
>(
|
||||
() => ({
|
||||
filter: {
|
||||
pageTypes
|
||||
}
|
||||
}),
|
||||
[pageTypes]
|
||||
);
|
||||
|
||||
const shouldSkipPageListQuery = !pageTypes.length || !isDeleteDialogOpen;
|
||||
|
||||
const {
|
||||
data: pagesAssignedToSelectedTypesData,
|
||||
loading: loadingPagesAssignedToSelectedTypes
|
||||
} = usePageCountQuery({
|
||||
variables: pagesAssignedToSelectedTypesQueryVars,
|
||||
skip: shouldSkipPageListQuery
|
||||
});
|
||||
|
||||
const selectedPagesAssignedToDeleteUrl = pageListUrl({
|
||||
pageTypes
|
||||
});
|
||||
|
||||
const assignedItemsCount =
|
||||
pagesAssignedToSelectedTypesData?.pages?.totalCount;
|
||||
|
||||
return {
|
||||
...messages,
|
||||
isOpen: isDeleteDialogOpen,
|
||||
assignedItemsCount,
|
||||
viewAssignedItemsUrl: selectedPagesAssignedToDeleteUrl,
|
||||
isLoading: loadingPagesAssignedToSelectedTypes,
|
||||
typesToDelete: pageTypes
|
||||
};
|
||||
}
|
||||
|
||||
export default usePageTypeDelete;
|
|
@ -4,6 +4,7 @@ import AssignAttributeDialog from "@saleor/components/AssignAttributeDialog";
|
|||
import AttributeUnassignDialog from "@saleor/components/AttributeUnassignDialog";
|
||||
import BulkAttributeUnassignDialog from "@saleor/components/BulkAttributeUnassignDialog";
|
||||
import NotFoundPage from "@saleor/components/NotFoundPage";
|
||||
import TypeDeleteWarningDialog from "@saleor/components/TypeDeleteWarningDialog";
|
||||
import { WindowTitle } from "@saleor/components/WindowTitle";
|
||||
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
|
||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||
|
@ -11,7 +12,6 @@ import useNavigator from "@saleor/hooks/useNavigator";
|
|||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getStringOrPlaceholder } from "@saleor/misc";
|
||||
import PageTypeDeleteDialog from "@saleor/pageTypes/components/PageTypeDeleteDialog";
|
||||
import {
|
||||
useAssignPageAttributeMutation,
|
||||
usePageTypeAttributeReorderMutation,
|
||||
|
@ -34,6 +34,7 @@ import PageTypeDetailsPage, {
|
|||
PageTypeForm
|
||||
} from "../components/PageTypeDetailsPage";
|
||||
import useAvailablePageAttributeSearch from "../hooks/useAvailablePageAttributeSearch";
|
||||
import usePageTypeDelete from "../hooks/usePageTypeDelete";
|
||||
import { usePageTypeDetailsQuery } from "../queries";
|
||||
import { pageTypeListUrl, pageTypeUrl, PageTypeUrlQueryParams } from "../urls";
|
||||
|
||||
|
@ -185,6 +186,11 @@ export const PageTypeDetails: React.FC<PageTypeDetailsProps> = ({
|
|||
|
||||
const loading = updatePageTypeOpts.loading || dataLoading;
|
||||
|
||||
const pageTypeDeleteData = usePageTypeDelete({
|
||||
singleId: id,
|
||||
params
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<WindowTitle title={data?.pageType.name} />
|
||||
|
@ -246,50 +252,55 @@ export const PageTypeDetails: React.FC<PageTypeDetailsProps> = ({
|
|||
)
|
||||
}}
|
||||
/>
|
||||
<PageTypeDeleteDialog
|
||||
confirmButtonState={deletePageTypeOpts.status}
|
||||
name={getStringOrPlaceholder(data?.pageType.name)}
|
||||
hasPages={data?.pageType.hasPages}
|
||||
open={params.action === "remove"}
|
||||
onClose={() => navigate(pageTypeUrl(id))}
|
||||
onConfirm={handlePageTypeDelete}
|
||||
/>
|
||||
|
||||
{!dataLoading && (
|
||||
<AssignAttributeDialog
|
||||
attributes={mapEdgesToItems(
|
||||
result?.data?.pageType?.availableAttributes
|
||||
)}
|
||||
confirmButtonState={assignAttributeOpts.status}
|
||||
errors={
|
||||
assignAttributeOpts.data?.pageAttributeAssign.errors
|
||||
? assignAttributeOpts.data.pageAttributeAssign.errors.map(err =>
|
||||
getPageErrorMessage(err, intl)
|
||||
)
|
||||
: []
|
||||
}
|
||||
loading={result.loading}
|
||||
onClose={closeModal}
|
||||
onSubmit={handleAssignAttribute}
|
||||
onFetch={search}
|
||||
onFetchMore={loadMore}
|
||||
onOpen={result.refetch}
|
||||
hasMore={
|
||||
!!result.data?.pageType.availableAttributes.pageInfo.hasNextPage
|
||||
}
|
||||
open={params.action === "assign-attribute"}
|
||||
selected={params.ids || []}
|
||||
onToggle={attributeId => {
|
||||
const ids = params.ids || [];
|
||||
navigate(
|
||||
pageTypeUrl(id, {
|
||||
...params,
|
||||
ids: ids.includes(attributeId)
|
||||
? params.ids.filter(selectedId => selectedId !== attributeId)
|
||||
: [...ids, attributeId]
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<TypeDeleteWarningDialog
|
||||
{...pageTypeDeleteData}
|
||||
typesData={[pageType]}
|
||||
typesToDelete={[id]}
|
||||
onClose={closeModal}
|
||||
onDelete={handlePageTypeDelete}
|
||||
deleteButtonState={deletePageTypeOpts.status}
|
||||
/>
|
||||
<AssignAttributeDialog
|
||||
attributes={result.data?.pageType.availableAttributes.edges.map(
|
||||
edge => edge.node
|
||||
)}
|
||||
confirmButtonState={assignAttributeOpts.status}
|
||||
errors={
|
||||
assignAttributeOpts.data?.pageAttributeAssign.errors
|
||||
? assignAttributeOpts.data.pageAttributeAssign.errors.map(err =>
|
||||
getPageErrorMessage(err, intl)
|
||||
)
|
||||
: []
|
||||
}
|
||||
loading={result.loading}
|
||||
onClose={closeModal}
|
||||
onSubmit={handleAssignAttribute}
|
||||
onFetch={search}
|
||||
onFetchMore={loadMore}
|
||||
onOpen={result.refetch}
|
||||
hasMore={
|
||||
!!result.data?.pageType.availableAttributes.pageInfo.hasNextPage
|
||||
}
|
||||
open={params.action === "assign-attribute"}
|
||||
selected={params.ids || []}
|
||||
onToggle={attributeId => {
|
||||
const ids = params.ids || [];
|
||||
navigate(
|
||||
pageTypeUrl(id, {
|
||||
...params,
|
||||
ids: ids.includes(attributeId)
|
||||
? params.ids.filter(
|
||||
selectedId => selectedId !== attributeId
|
||||
)
|
||||
: [...ids, attributeId]
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<BulkAttributeUnassignDialog
|
||||
title={intl.formatMessage({
|
||||
|
|
|
@ -4,6 +4,7 @@ import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog";
|
|||
import SaveFilterTabDialog, {
|
||||
SaveFilterTabDialogFormData
|
||||
} from "@saleor/components/SaveFilterTabDialog";
|
||||
import TypeDeleteWarningDialog from "@saleor/components/TypeDeleteWarningDialog";
|
||||
import useBulkActions from "@saleor/hooks/useBulkActions";
|
||||
import useListSettings from "@saleor/hooks/useListSettings";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
|
@ -13,7 +14,7 @@ import usePaginator, {
|
|||
} from "@saleor/hooks/usePaginator";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { getStringOrPlaceholder } from "@saleor/misc";
|
||||
import PageTypeBulkDeleteDialog from "@saleor/pageTypes/components/PageTypeBulkDeleteDialog";
|
||||
import usePageTypeDelete from "@saleor/pageTypes/hooks/usePageTypeDelete";
|
||||
import { usePageTypeBulkDeleteMutation } from "@saleor/pageTypes/mutations";
|
||||
import { ListViews } from "@saleor/types";
|
||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
|
@ -51,9 +52,13 @@ export const PageTypeList: React.FC<PageTypeListProps> = ({ params }) => {
|
|||
const navigate = useNavigator();
|
||||
const paginate = usePaginator();
|
||||
const notify = useNotifier();
|
||||
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
|
||||
params.ids
|
||||
);
|
||||
const {
|
||||
isSelected,
|
||||
listElements: selectedPageTypes,
|
||||
reset,
|
||||
toggle,
|
||||
toggleAll
|
||||
} = useBulkActions(params.ids);
|
||||
const intl = useIntl();
|
||||
const { settings } = useListSettings(ListViews.PAGES_LIST);
|
||||
|
||||
|
@ -155,10 +160,12 @@ export const PageTypeList: React.FC<PageTypeListProps> = ({ params }) => {
|
|||
}
|
||||
});
|
||||
|
||||
const selectedPageTypesHasPages = data?.pageTypes.edges.some(
|
||||
pageType =>
|
||||
pageType.node.hasPages && params.ids?.some(id => id === pageType.node.id)
|
||||
);
|
||||
const pageTypeDeleteData = usePageTypeDelete({
|
||||
selectedTypes: selectedPageTypes,
|
||||
params
|
||||
});
|
||||
|
||||
const pageTypesData = data?.pageTypes?.edges.map(edge => edge.node) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -181,7 +188,7 @@ export const PageTypeList: React.FC<PageTypeListProps> = ({ params }) => {
|
|||
onRowClick={id => () => navigate(pageTypeUrl(id))}
|
||||
onSort={handleSort}
|
||||
isChecked={isSelected}
|
||||
selected={listElements.length}
|
||||
selected={selectedPageTypes.length}
|
||||
sort={getSortParams(params)}
|
||||
toggle={toggle}
|
||||
toggleAll={toggleAll}
|
||||
|
@ -190,7 +197,7 @@ export const PageTypeList: React.FC<PageTypeListProps> = ({ params }) => {
|
|||
color="primary"
|
||||
onClick={() =>
|
||||
openModal("remove", {
|
||||
ids: listElements
|
||||
ids: selectedPageTypes
|
||||
})
|
||||
}
|
||||
>
|
||||
|
@ -198,13 +205,14 @@ export const PageTypeList: React.FC<PageTypeListProps> = ({ params }) => {
|
|||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<PageTypeBulkDeleteDialog
|
||||
confirmButtonState={pageTypeBulkDeleteOpts.status}
|
||||
quantity={params.ids?.length}
|
||||
hasPages={selectedPageTypesHasPages}
|
||||
open={params.action === "remove"}
|
||||
<TypeDeleteWarningDialog
|
||||
{...pageTypeDeleteData}
|
||||
typesData={pageTypesData}
|
||||
typesToDelete={selectedPageTypes}
|
||||
onClose={closeModal}
|
||||
onConfirm={hanldePageTypeBulkDelete}
|
||||
onDelete={hanldePageTypeBulkDelete}
|
||||
deleteButtonState={pageTypeBulkDeleteOpts.status}
|
||||
showViewAssignedItemsButton={false}
|
||||
/>
|
||||
<SaveFilterTabDialog
|
||||
open={params.action === "save-search"}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { pageDetailsFragment, pageFragment } from "@saleor/fragments/pages";
|
|||
import makeQuery from "@saleor/hooks/makeQuery";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { PageCount, PageCountVariables } from "./types/PageCount";
|
||||
import { PageDetails, PageDetailsVariables } from "./types/PageDetails";
|
||||
import { PageList, PageListVariables } from "./types/PageList";
|
||||
|
||||
|
@ -50,3 +51,15 @@ const pageDetails = gql`
|
|||
export const usePageDetailsQuery = makeQuery<PageDetails, PageDetailsVariables>(
|
||||
pageDetails
|
||||
);
|
||||
|
||||
const pageCountQuery = gql`
|
||||
query PageCount($filter: PageFilterInput) {
|
||||
pages(filter: $filter) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const usePageCountQuery = makeQuery<PageCount, PageCountVariables>(
|
||||
pageCountQuery
|
||||
);
|
||||
|
|
23
src/pages/types/PageCount.ts
Normal file
23
src/pages/types/PageCount.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { PageFilterInput } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: PageCount
|
||||
// ====================================================
|
||||
|
||||
export interface PageCount_pages {
|
||||
__typename: "PageCountableConnection";
|
||||
totalCount: number | null;
|
||||
}
|
||||
|
||||
export interface PageCount {
|
||||
pages: PageCount_pages | null;
|
||||
}
|
||||
|
||||
export interface PageCountVariables {
|
||||
filter?: PageFilterInput | null;
|
||||
}
|
|
@ -1,7 +1,14 @@
|
|||
import { stringify as stringifyQs } from "qs";
|
||||
import urlJoin from "url-join";
|
||||
|
||||
import { BulkAction, Dialog, Pagination, SingleAction, Sort } from "../types";
|
||||
import {
|
||||
BulkAction,
|
||||
Dialog,
|
||||
FiltersWithMultipleValues,
|
||||
Pagination,
|
||||
SingleAction,
|
||||
Sort
|
||||
} from "../types";
|
||||
|
||||
export const pagesSection = "/pages/";
|
||||
|
||||
|
@ -12,8 +19,17 @@ export enum PageListUrlSortField {
|
|||
slug = "slug",
|
||||
visible = "visible"
|
||||
}
|
||||
|
||||
export enum PageListUrlFiltersWithMultipleValues {
|
||||
pageTypes = "pageTypes"
|
||||
}
|
||||
|
||||
export type PageListUrlFilters = FiltersWithMultipleValues<
|
||||
PageListUrlFiltersWithMultipleValues
|
||||
>;
|
||||
export type PageListUrlSort = Sort<PageListUrlSortField>;
|
||||
export type PageListUrlQueryParams = BulkAction &
|
||||
PageListUrlFilters &
|
||||
Dialog<PageListUrlDialog> &
|
||||
PageListUrlSort &
|
||||
Pagination;
|
||||
|
|
|
@ -4,8 +4,8 @@ import CardContent from "@material-ui/core/CardContent";
|
|||
import Typography from "@material-ui/core/Typography";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import { PluginConfigurationFragment_configuration } from "@saleor/fragments/types/PluginConfigurationFragment";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import { Plugin_plugin_configuration } from "@saleor/plugins/types/Plugin";
|
||||
import { isSecretField } from "@saleor/plugins/utils";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes";
|
||||
|
@ -13,7 +13,7 @@ import React from "react";
|
|||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
interface PluginAuthorizationProps {
|
||||
fields: Plugin_plugin_configuration[];
|
||||
fields: PluginConfigurationFragment_configuration[];
|
||||
onClear: (field: string) => void;
|
||||
onEdit: (field: string) => void;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { Card } from "@material-ui/core";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import PluginDetailsChannelsCardContent, {
|
||||
PluginDetailsChannelsCardProps
|
||||
} from "./PluginDetailsChannelsCardContent";
|
||||
|
||||
const PluginDetailsChannelsCard: React.FC<PluginDetailsChannelsCardProps> = props => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle title={intl.formatMessage(sectionNames.channels)} />
|
||||
<PluginDetailsChannelsCardContent {...props} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginDetailsChannelsCard;
|
|
@ -0,0 +1,83 @@
|
|||
import { CardContent, makeStyles, Typography } from "@material-ui/core";
|
||||
import CollectionWithDividers from "@saleor/components/CollectionWithDividers";
|
||||
import Skeleton from "@saleor/components/Skeleton";
|
||||
import { Plugin_plugin } from "@saleor/plugins/types/Plugin";
|
||||
import { isPluginGlobal } from "@saleor/plugins/views/utils";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
itemContainer: {
|
||||
position: "relative",
|
||||
cursor: "pointer"
|
||||
},
|
||||
itemActiveIndicator: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
width: 2,
|
||||
height: "100%"
|
||||
}
|
||||
}),
|
||||
{ name: "PluginDetailsChannelsCardContent" }
|
||||
);
|
||||
|
||||
export interface PluginDetailsChannelsCardProps {
|
||||
setSelectedChannelId: (channelId: string) => void;
|
||||
selectedChannelId: string;
|
||||
plugin: Plugin_plugin;
|
||||
}
|
||||
|
||||
import { pluginDetailsChannelsCardMessages as messages } from "./messages";
|
||||
|
||||
const PluginDetailsChannelsCardContent: React.FC<PluginDetailsChannelsCardProps> = ({
|
||||
plugin,
|
||||
selectedChannelId,
|
||||
setSelectedChannelId
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
|
||||
if (!plugin) {
|
||||
return (
|
||||
<CardContent>
|
||||
<Skeleton />
|
||||
</CardContent>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPluginGlobal(plugin.globalConfiguration)) {
|
||||
return (
|
||||
<CardContent>
|
||||
<FormattedMessage {...messages.noChannelsSubtitle} />
|
||||
</CardContent>
|
||||
);
|
||||
}
|
||||
|
||||
const isChannelSelected = (channelId: string) =>
|
||||
selectedChannelId === channelId;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CollectionWithDividers
|
||||
collection={plugin.channelConfigurations}
|
||||
renderItem={({ channel }) => (
|
||||
<div
|
||||
className={classes.itemContainer}
|
||||
key={channel.id}
|
||||
onClick={() => setSelectedChannelId(channel.id)}
|
||||
>
|
||||
{isChannelSelected(channel.id) && (
|
||||
<div className={classes.itemActiveIndicator}></div>
|
||||
)}
|
||||
<CardContent>
|
||||
<Typography>{channel.name}</Typography>
|
||||
</CardContent>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginDetailsChannelsCardContent;
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./PluginDetailsChannelsCard";
|
||||
export { default } from "./PluginDetailsChannelsCard";
|
|
@ -0,0 +1,8 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const pluginDetailsChannelsCardMessages = defineMessages({
|
||||
noChannelsSubtitle: {
|
||||
defaultMessage: "Plugin’s settings are set common across all channels",
|
||||
description: "PluginDetailsChannelsCard no channels subtitle"
|
||||
}
|
||||
});
|
|
@ -9,9 +9,9 @@ import ConfirmButton, {
|
|||
} from "@saleor/components/ConfirmButton";
|
||||
import Form from "@saleor/components/Form";
|
||||
import Skeleton from "@saleor/components/Skeleton";
|
||||
import { PluginConfigurationFragment_configuration } from "@saleor/fragments/types/PluginConfigurationFragment";
|
||||
import { buttonMessages } from "@saleor/intl";
|
||||
import { maybe } from "@saleor/misc";
|
||||
import { Plugin_plugin_configuration } from "@saleor/plugins/types/Plugin";
|
||||
import { DialogProps } from "@saleor/types";
|
||||
import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes";
|
||||
import React from "react";
|
||||
|
@ -22,7 +22,7 @@ export interface PluginSecretFieldDialogFormData {
|
|||
}
|
||||
export interface PluginSecretFieldDialogProps extends DialogProps {
|
||||
confirmButtonState: ConfirmButtonTransitionState;
|
||||
field: Plugin_plugin_configuration;
|
||||
field: PluginConfigurationFragment_configuration;
|
||||
onConfirm: (data: PluginSecretFieldDialogFormData) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import Typography from "@material-ui/core/Typography";
|
|||
import InfoIcon from "@material-ui/icons/Info";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import ControlledSwitch from "@saleor/components/ControlledSwitch";
|
||||
import { Plugin_plugin_configuration } from "@saleor/plugins/types/Plugin";
|
||||
import { PluginConfigurationFragment_configuration } from "@saleor/fragments/types/PluginConfigurationFragment";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import { UserError } from "@saleor/types";
|
||||
import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes";
|
||||
|
@ -21,7 +21,7 @@ interface PluginSettingsProps {
|
|||
errors: UserError[];
|
||||
disabled: boolean;
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
fields: Plugin_plugin_configuration[];
|
||||
fields: PluginConfigurationFragment_configuration[];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import Typography from "@material-ui/core/Typography";
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import CardSpacer from "@saleor/components/CardSpacer";
|
||||
// import CardSpacer from "@saleor/components/CardSpacer";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
|
||||
import Container from "@saleor/components/Container";
|
||||
import Form from "@saleor/components/Form";
|
||||
import Grid from "@saleor/components/Grid";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import PageHeader from "@saleor/components/PageHeader";
|
||||
import SaveButtonBar from "@saleor/components/SaveButtonBar";
|
||||
import { PluginErrorFragment } from "@saleor/fragments/types/PluginErrorFragment";
|
||||
|
@ -13,15 +12,16 @@ import { ChangeEvent } from "@saleor/hooks/useForm";
|
|||
import { sectionNames } from "@saleor/intl";
|
||||
import { getStringOrPlaceholder } from "@saleor/misc";
|
||||
import { isSecretField } from "@saleor/plugins/utils";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import { ConfigurationItemInput } from "@saleor/types/globalTypes";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { Plugin_plugin } from "../../types/Plugin";
|
||||
import PluginAuthorization from "../PluginAuthorization";
|
||||
import PluginDetailsChannelsCard from "../PluginDetailsChannelsCard";
|
||||
import PluginInfo from "../PluginInfo";
|
||||
import PluginSettings from "../PluginSettings";
|
||||
import { PluginConfiguration } from "./types";
|
||||
|
||||
export interface PluginDetailsPageFormData {
|
||||
active: boolean;
|
||||
|
@ -31,51 +31,50 @@ export interface PluginDetailsPageFormData {
|
|||
export interface PluginsDetailsPageProps {
|
||||
disabled: boolean;
|
||||
errors: PluginErrorFragment[];
|
||||
plugin: Plugin_plugin;
|
||||
plugin?: Plugin_plugin;
|
||||
saveButtonBarState: ConfirmButtonTransitionState;
|
||||
onBack: () => void;
|
||||
onClear: (field: string) => void;
|
||||
onEdit: (field: string) => void;
|
||||
onSubmit: (data: PluginDetailsPageFormData) => void;
|
||||
selectedConfig?: PluginConfiguration;
|
||||
setSelectedChannelId: (channelId: string) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
{
|
||||
spacer: {
|
||||
gridColumnEnd: "span 2"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "PluginsDetailsPage"
|
||||
}
|
||||
);
|
||||
|
||||
const PluginsDetailsPage: React.FC<PluginsDetailsPageProps> = props => {
|
||||
const {
|
||||
disabled,
|
||||
errors,
|
||||
plugin,
|
||||
saveButtonBarState,
|
||||
onBack,
|
||||
onClear,
|
||||
onEdit,
|
||||
onSubmit
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const PluginsDetailsPage: React.FC<PluginsDetailsPageProps> = ({
|
||||
disabled,
|
||||
errors,
|
||||
plugin,
|
||||
saveButtonBarState,
|
||||
onBack,
|
||||
onClear,
|
||||
onEdit,
|
||||
onSubmit,
|
||||
selectedConfig,
|
||||
setSelectedChannelId
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const initialForm: PluginDetailsPageFormData = {
|
||||
active: plugin?.active || false,
|
||||
configuration: plugin?.configuration
|
||||
?.filter(field => !isSecretField(plugin?.configuration || [], field.name))
|
||||
|
||||
const initialFormData = (): PluginDetailsPageFormData => ({
|
||||
active: selectedConfig?.active,
|
||||
configuration: selectedConfig?.configuration
|
||||
?.filter(
|
||||
field => !isSecretField(selectedConfig?.configuration || [], field.name)
|
||||
)
|
||||
.map(field => ({
|
||||
...field,
|
||||
value: field.value || ""
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
const selectedChannelId = selectedConfig?.channel?.id;
|
||||
|
||||
return (
|
||||
<Form initial={initialForm} onSubmit={onSubmit}>
|
||||
<Form
|
||||
initial={initialFormData()}
|
||||
onSubmit={onSubmit}
|
||||
key={selectedChannelId}
|
||||
>
|
||||
{({ data, hasChanged, submit, set }) => {
|
||||
const onChange = (event: ChangeEvent) => {
|
||||
const { name, value } = event.target;
|
||||
|
@ -91,7 +90,7 @@ const PluginsDetailsPage: React.FC<PluginsDetailsPageProps> = props => {
|
|||
}
|
||||
});
|
||||
|
||||
plugin.configuration.map(item => {
|
||||
selectedConfig.configuration.map(item => {
|
||||
if (item.name === name) {
|
||||
item.value = value;
|
||||
}
|
||||
|
@ -118,60 +117,45 @@ const PluginsDetailsPage: React.FC<PluginsDetailsPageProps> = props => {
|
|||
/>
|
||||
<Grid variant="inverted">
|
||||
<div>
|
||||
<Typography variant="h6">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Plugin Information and Status",
|
||||
description: "section header"
|
||||
})}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{intl.formatMessage({
|
||||
defaultMessage:
|
||||
"These are general information about your store. They define what is the URL of your store and what is shown in browsers taskbar."
|
||||
})}
|
||||
</Typography>
|
||||
<PluginDetailsChannelsCard
|
||||
plugin={plugin}
|
||||
selectedChannelId={selectedChannelId}
|
||||
setSelectedChannelId={setSelectedChannelId}
|
||||
/>
|
||||
</div>
|
||||
<PluginInfo
|
||||
data={data}
|
||||
description={plugin?.description || ""}
|
||||
errors={errors}
|
||||
name={plugin?.name || ""}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{data.configuration && (
|
||||
<>
|
||||
<Hr className={classes.spacer} />
|
||||
<div>
|
||||
<Typography variant="h6">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Plugin Settings",
|
||||
description: "section header"
|
||||
})}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<PluginInfo
|
||||
data={data}
|
||||
description={plugin?.description || ""}
|
||||
errors={errors}
|
||||
name={plugin?.name || ""}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<CardSpacer />
|
||||
{data.configuration && (
|
||||
<div>
|
||||
<PluginSettings
|
||||
data={data}
|
||||
fields={plugin?.configuration || []}
|
||||
fields={selectedConfig?.configuration || []}
|
||||
errors={errors}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
/>
|
||||
{plugin?.configuration.some(field =>
|
||||
isSecretField(plugin.configuration, field.name)
|
||||
{selectedConfig?.configuration.some(field =>
|
||||
isSecretField(selectedConfig?.configuration, field.name)
|
||||
) && (
|
||||
<>
|
||||
<CardSpacer />
|
||||
<PluginAuthorization
|
||||
fields={plugin.configuration}
|
||||
fields={selectedConfig?.configuration}
|
||||
onClear={onClear}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</Grid>
|
||||
<SaveButtonBar
|
||||
disabled={disabled || !hasChanged}
|
||||
|
@ -185,5 +169,5 @@ const PluginsDetailsPage: React.FC<PluginsDetailsPageProps> = props => {
|
|||
</Form>
|
||||
);
|
||||
};
|
||||
PluginsDetailsPage.displayName = "PluginsDetailsPage";
|
||||
|
||||
export default PluginsDetailsPage;
|
||||
|
|
3
src/plugins/components/PluginsDetailsPage/types.ts
Normal file
3
src/plugins/components/PluginsDetailsPage/types.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { PluginConfigurationFragment } from "@saleor/fragments/types/PluginConfigurationFragment";
|
||||
|
||||
export type PluginConfiguration = PluginConfigurationFragment;
|
|
@ -0,0 +1,75 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import StatusLabel from "@saleor/components/StatusLabel";
|
||||
import { statusLabelMessages } from "@saleor/components/StatusLabel/messages";
|
||||
import { Plugins_plugins_edges_node } from "@saleor/plugins/types/Plugins";
|
||||
import { isPluginGlobal } from "@saleor/plugins/views/utils";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { pluginAvailabilityStatusMessages as messages } from "./messages";
|
||||
import {
|
||||
getActiveChannelConfigsCount,
|
||||
getAllChannelConfigsCount
|
||||
} from "./utils";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
() => ({
|
||||
horizontalContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "row"
|
||||
}
|
||||
}),
|
||||
{ name: "ChannelStatusLabel" }
|
||||
);
|
||||
|
||||
interface PluginAvailabilityStatusProps {
|
||||
plugin: Plugins_plugins_edges_node;
|
||||
}
|
||||
|
||||
const PluginAvailabilityStatus: React.FC<PluginAvailabilityStatusProps> = ({
|
||||
plugin: { globalConfiguration, channelConfigurations }
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
const intl = useIntl();
|
||||
|
||||
const isGlobalPlugin = isPluginGlobal(globalConfiguration);
|
||||
|
||||
const activeChannelsCount = getActiveChannelConfigsCount(
|
||||
channelConfigurations
|
||||
);
|
||||
|
||||
const isStatusActive = isGlobalPlugin
|
||||
? globalConfiguration.active
|
||||
: !!activeChannelsCount;
|
||||
|
||||
const globalPluginLabel = intl.formatMessage(
|
||||
isStatusActive
|
||||
? statusLabelMessages.active
|
||||
: statusLabelMessages.deactivated
|
||||
);
|
||||
|
||||
return (
|
||||
<StatusLabel
|
||||
label={
|
||||
isGlobalPlugin ? (
|
||||
globalPluginLabel
|
||||
) : (
|
||||
<div className={classes.horizontalContainer}>
|
||||
<Typography>
|
||||
{intl.formatMessage(messages.channelTitle, {
|
||||
activeChannelsCount
|
||||
})}
|
||||
</Typography>
|
||||
<Typography color="textSecondary">{`/${getAllChannelConfigsCount(
|
||||
channelConfigurations
|
||||
)}`}</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
status={isStatusActive ? "success" : "error"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginAvailabilityStatus;
|
|
@ -0,0 +1,76 @@
|
|||
import { CardContent, Divider, Typography } from "@material-ui/core";
|
||||
import CardSpacer from "@saleor/components/CardSpacer";
|
||||
import CollectionWithDividers from "@saleor/components/CollectionWithDividers";
|
||||
import StatusLabel from "@saleor/components/StatusLabel";
|
||||
import { statusLabelMessages } from "@saleor/components/StatusLabel/messages";
|
||||
import { Plugin_plugin } from "@saleor/plugins/types/Plugin";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { channelConfigPluginMessages as messages } from "../messages";
|
||||
import {
|
||||
getActiveChannelConfigsCount,
|
||||
getAllChannelConfigsCount
|
||||
} from "../utils";
|
||||
import ScrollableContent from "./ScrollableContent";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
itemContainer: {
|
||||
padding: theme.spacing(0, 1)
|
||||
}
|
||||
}),
|
||||
{ name: "ChannelConfigPluginPopupBody" }
|
||||
);
|
||||
|
||||
interface ChannelConfigPluginPopupBodyProps {
|
||||
plugin: Plugin_plugin;
|
||||
}
|
||||
|
||||
const ChannelConfigPluginPopupBody: React.FC<ChannelConfigPluginPopupBodyProps> = ({
|
||||
plugin: { channelConfigurations }
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardContent>
|
||||
<Typography>
|
||||
{intl.formatMessage(messages.title, {
|
||||
allChannelsCount: getAllChannelConfigsCount(channelConfigurations),
|
||||
activeChannelsCount: getActiveChannelConfigsCount(
|
||||
channelConfigurations
|
||||
)
|
||||
})}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<ScrollableContent>
|
||||
<CardSpacer />
|
||||
<CollectionWithDividers
|
||||
collection={channelConfigurations}
|
||||
DividerComponent={CardSpacer}
|
||||
renderItem={({ channel, active }) => (
|
||||
<div className={classes.itemContainer}>
|
||||
<StatusLabel
|
||||
key={channel.id}
|
||||
label={channel.name}
|
||||
status={active ? "success" : "error"}
|
||||
subtitle={intl.formatMessage(
|
||||
active
|
||||
? statusLabelMessages.active
|
||||
: statusLabelMessages.inactive
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<CardSpacer />
|
||||
</ScrollableContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelConfigPluginPopupBody;
|
|
@ -0,0 +1,42 @@
|
|||
import { CardContent, Typography } from "@material-ui/core";
|
||||
import CardSpacer from "@saleor/components/CardSpacer";
|
||||
import StatusLabel from "@saleor/components/StatusLabel";
|
||||
import { statusLabelMessages } from "@saleor/components/StatusLabel/messages";
|
||||
import { Plugin_plugin } from "@saleor/plugins/types/Plugin";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { globalConfigPluginMessages as messages } from "../messages";
|
||||
|
||||
interface GlobalConfigPluginPopupBodyProps {
|
||||
plugin: Plugin_plugin;
|
||||
}
|
||||
|
||||
const GlobalConfigPluginPopupBody: React.FC<GlobalConfigPluginPopupBodyProps> = ({
|
||||
plugin
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { active } = plugin.globalConfiguration;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardContent>
|
||||
<Typography>{intl.formatMessage(messages.title)}</Typography>
|
||||
<CardSpacer />
|
||||
<Typography variant="caption">
|
||||
{intl.formatMessage(messages.description)}
|
||||
</Typography>
|
||||
<CardSpacer />
|
||||
<StatusLabel
|
||||
status={active ? "success" : "error"}
|
||||
label={intl.formatMessage(
|
||||
active ? statusLabelMessages.active : statusLabelMessages.inactive
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalConfigPluginPopupBody;
|
|
@ -0,0 +1,52 @@
|
|||
import { Card, makeStyles, Popper } from "@material-ui/core";
|
||||
import { Plugin_plugin } from "@saleor/plugins/types/Plugin";
|
||||
import { isPluginGlobal } from "@saleor/plugins/views/utils";
|
||||
import React from "react";
|
||||
|
||||
import ChannelConfigPluginPopupBody from "./ChannelConfigPluginPopupBody";
|
||||
import GlobalConfigPluginPopupBody from "./GlobalConfigPluginPopupBody";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
() => ({
|
||||
container: {
|
||||
maxWidth: 300,
|
||||
zIndex: 1000
|
||||
}
|
||||
}),
|
||||
{ name: "PluginChannelsAvailabilityStatusPopup" }
|
||||
);
|
||||
|
||||
interface PluginAvailabilityStatusPopupProps {
|
||||
plugin: Plugin_plugin;
|
||||
isOpen: boolean;
|
||||
anchor: React.RefObject<HTMLTableCellElement>;
|
||||
}
|
||||
|
||||
const PluginAvailabilityStatusPopup: React.FC<PluginAvailabilityStatusPopupProps> = ({
|
||||
plugin,
|
||||
isOpen,
|
||||
anchor
|
||||
}) => {
|
||||
const classes = useStyles({});
|
||||
|
||||
const isGlobalPlugin = isPluginGlobal(plugin.globalConfiguration);
|
||||
|
||||
return (
|
||||
<Popper
|
||||
placement="left"
|
||||
open={isOpen}
|
||||
className={classes.container}
|
||||
anchorEl={anchor.current}
|
||||
>
|
||||
<Card>
|
||||
{isGlobalPlugin ? (
|
||||
<GlobalConfigPluginPopupBody plugin={plugin} />
|
||||
) : (
|
||||
<ChannelConfigPluginPopupBody plugin={plugin} />
|
||||
)}
|
||||
</Card>
|
||||
</Popper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginAvailabilityStatusPopup;
|
|
@ -0,0 +1,29 @@
|
|||
import List from "@material-ui/core/List";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import React from "react";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
() => ({
|
||||
container: {
|
||||
maxHeight: 450,
|
||||
overflow: "auto"
|
||||
}
|
||||
}),
|
||||
{ name: "ScrollableContent" }
|
||||
);
|
||||
|
||||
interface ScrollableContentProps {
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
}
|
||||
|
||||
const ScrollableContent: React.FC<ScrollableContentProps> = ({ children }) => {
|
||||
const classes = useStyles({});
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<List>{children}</List>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollableContent;
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./PluginAvailabilityStatusPopup";
|
||||
export { default } from "./PluginAvailabilityStatusPopup";
|
|
@ -0,0 +1,46 @@
|
|||
import { TableCell } from "@material-ui/core";
|
||||
import Skeleton from "@saleor/components/Skeleton";
|
||||
import { Plugin_plugin } from "@saleor/plugins/types/Plugin";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import PluginAvailabilityStatus from "./PluginAvailabilityStatus";
|
||||
import PluginAvailabilityStatusPopup from "./PluginAvailabilityStatusPopup";
|
||||
|
||||
interface PluginChannelAvailabilityCellProps {
|
||||
plugin: Plugin_plugin;
|
||||
}
|
||||
|
||||
const PluginChannelAvailabilityCell: React.FC<PluginChannelAvailabilityCellProps> = ({
|
||||
plugin
|
||||
}) => {
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
const popupAnchor = useRef<HTMLTableCellElement>(null);
|
||||
|
||||
const handleMouseOver = () => setIsPopupOpen(true);
|
||||
|
||||
const handleMouseLeave = () => setIsPopupOpen(false);
|
||||
|
||||
return (
|
||||
<TableCell
|
||||
colSpan={2}
|
||||
ref={popupAnchor}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{plugin ? (
|
||||
<>
|
||||
<PluginAvailabilityStatus plugin={plugin} />
|
||||
<PluginAvailabilityStatusPopup
|
||||
plugin={plugin}
|
||||
isOpen={isPopupOpen}
|
||||
anchor={popupAnchor}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginChannelAvailabilityCell;
|
|
@ -0,0 +1,27 @@
|
|||
import { TableCell } from "@material-ui/core";
|
||||
import { Plugin_plugin } from "@saleor/plugins/types/Plugin";
|
||||
import { isPluginGlobal } from "@saleor/plugins/views/utils";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { pluginChannelConfigurationCellMessages as messages } from "./messages";
|
||||
|
||||
interface PluginChannelConfigurationCellProps {
|
||||
plugin: Plugin_plugin;
|
||||
}
|
||||
|
||||
const PluginChannelConfigurationCell: React.FC<PluginChannelConfigurationCellProps> = ({
|
||||
plugin
|
||||
}) => {
|
||||
const message = isPluginGlobal(plugin.globalConfiguration)
|
||||
? messages.globalLabel
|
||||
: messages.channelLabel;
|
||||
|
||||
return (
|
||||
<TableCell colSpan={2}>
|
||||
<FormattedMessage {...message} />
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginChannelConfigurationCell;
|
52
src/plugins/components/PluginsList/PluginListTableHead.tsx
Normal file
52
src/plugins/components/PluginsList/PluginListTableHead.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { TableHead } from "@material-ui/core";
|
||||
import TableCellHeader from "@saleor/components/TableCellHeader";
|
||||
import { PluginListUrlSortField } from "@saleor/plugins/urls";
|
||||
import { SortPage } from "@saleor/types";
|
||||
import { getArrowDirection } from "@saleor/utils/sort";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { pluginsListTableHeadMessages as messages } from "./messages";
|
||||
|
||||
type PluginListTableHeadProps = SortPage<PluginListUrlSortField>;
|
||||
|
||||
const PluginListTableHead: React.FC<PluginListTableHeadProps> = ({
|
||||
sort,
|
||||
onSort
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableCellHeader
|
||||
direction={
|
||||
sort.sort === PluginListUrlSortField.name
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
arrowPosition="right"
|
||||
onClick={() => onSort(PluginListUrlSortField.name)}
|
||||
colSpan={5}
|
||||
>
|
||||
{intl.formatMessage(messages.nameLabel)}
|
||||
</TableCellHeader>
|
||||
<TableCellHeader
|
||||
colSpan={2}
|
||||
direction={
|
||||
sort.sort === PluginListUrlSortField.active
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
onClick={() => onSort(PluginListUrlSortField.active)}
|
||||
>
|
||||
{intl.formatMessage(messages.confLabel)}
|
||||
</TableCellHeader>
|
||||
<TableCellHeader colSpan={2}>
|
||||
{intl.formatMessage(messages.channelLabel)}
|
||||
</TableCellHeader>
|
||||
<TableCellHeader></TableCellHeader>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginListTableHead;
|
|
@ -1,44 +1,25 @@
|
|||
import { makeStyles } from "@material-ui/core";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import TableFooter from "@material-ui/core/TableFooter";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import ResponsiveTable from "@saleor/components/ResponsiveTable";
|
||||
import Skeleton from "@saleor/components/Skeleton";
|
||||
import StatusLabel from "@saleor/components/StatusLabel";
|
||||
import TableCellHeader from "@saleor/components/TableCellHeader";
|
||||
import TablePagination from "@saleor/components/TablePagination";
|
||||
import { translateBoolean } from "@saleor/intl";
|
||||
import { maybe, renderCollection } from "@saleor/misc";
|
||||
import { renderCollection } from "@saleor/misc";
|
||||
import { PluginListUrlSortField } from "@saleor/plugins/urls";
|
||||
import { makeStyles } from "@saleor/theme";
|
||||
import { ListProps, SortPage } from "@saleor/types";
|
||||
import { getArrowDirection } from "@saleor/utils/sort";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { Plugins_plugins_edges_node } from "../../types/Plugins";
|
||||
import PluginChannelAvailabilityCell from "./PluginChannelAvailabilityCell";
|
||||
import PluginChannelConfigurationCell from "./PluginChannelConfigurationCell";
|
||||
import PluginListTableHead from "./PluginListTableHead";
|
||||
|
||||
export interface PluginListProps
|
||||
extends ListProps,
|
||||
SortPage<PluginListUrlSortField> {
|
||||
plugins: Plugins_plugins_edges_node[];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
colAction: {
|
||||
"& svg": {
|
||||
color: theme.palette.primary.main
|
||||
},
|
||||
textAlign: "right",
|
||||
width: 200
|
||||
},
|
||||
colActive: {
|
||||
width: 200
|
||||
},
|
||||
colName: {},
|
||||
export const useStyles = makeStyles(
|
||||
() => ({
|
||||
link: {
|
||||
cursor: "pointer"
|
||||
}
|
||||
|
@ -46,7 +27,13 @@ const useStyles = makeStyles(
|
|||
{ name: "PluginsList" }
|
||||
);
|
||||
|
||||
const numberOfColumns = 3;
|
||||
export interface PluginListProps
|
||||
extends ListProps,
|
||||
SortPage<PluginListUrlSortField> {
|
||||
plugins: Plugins_plugins_edges_node[];
|
||||
}
|
||||
|
||||
const totalColSpan = 10;
|
||||
|
||||
const PluginList: React.FC<PluginListProps> = props => {
|
||||
const {
|
||||
|
@ -66,47 +53,11 @@ const PluginList: React.FC<PluginListProps> = props => {
|
|||
|
||||
return (
|
||||
<ResponsiveTable>
|
||||
<TableHead>
|
||||
<TableCellHeader
|
||||
direction={
|
||||
sort.sort === PluginListUrlSortField.name
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
arrowPosition="right"
|
||||
onClick={() => onSort(PluginListUrlSortField.name)}
|
||||
className={classes.colName}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Name",
|
||||
description: "plugin name"
|
||||
})}
|
||||
</TableCellHeader>
|
||||
<TableCellHeader
|
||||
direction={
|
||||
sort.sort === PluginListUrlSortField.active
|
||||
? getArrowDirection(sort.asc)
|
||||
: undefined
|
||||
}
|
||||
onClick={() => onSort(PluginListUrlSortField.active)}
|
||||
className={classes.colActive}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Active",
|
||||
description: "plugin status"
|
||||
})}
|
||||
</TableCellHeader>
|
||||
<TableCell className={classes.colAction}>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Action",
|
||||
description: "user action bar"
|
||||
})}
|
||||
</TableCell>
|
||||
</TableHead>
|
||||
<PluginListTableHead sort={sort} onSort={onSort} />
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TablePagination
|
||||
colSpan={numberOfColumns}
|
||||
colSpan={totalColSpan}
|
||||
settings={settings}
|
||||
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
|
||||
onNextPage={onNextPage}
|
||||
|
@ -121,37 +72,33 @@ const PluginList: React.FC<PluginListProps> = props => {
|
|||
<TableBody>
|
||||
{renderCollection(
|
||||
plugins,
|
||||
plugin => (
|
||||
<TableRow
|
||||
hover={!!plugin}
|
||||
className={!!plugin ? classes.link : undefined}
|
||||
onClick={plugin ? onRowClick(plugin.id) : undefined}
|
||||
key={plugin ? plugin.id : "skeleton"}
|
||||
>
|
||||
<TableCell className={classes.colName}>
|
||||
{maybe<React.ReactNode>(() => plugin.name, <Skeleton />)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.colActive}>
|
||||
{maybe<React.ReactNode>(
|
||||
() => (
|
||||
<StatusLabel
|
||||
label={translateBoolean(plugin.active, intl)}
|
||||
status={plugin.active ? "success" : "error"}
|
||||
/>
|
||||
),
|
||||
plugin =>
|
||||
plugin ? (
|
||||
<TableRow
|
||||
hover={!!plugin}
|
||||
className={!!plugin ? classes.link : undefined}
|
||||
onClick={plugin ? onRowClick(plugin.id) : undefined}
|
||||
key={plugin ? plugin.id : "skeleton"}
|
||||
>
|
||||
<TableCell colSpan={5}>{plugin.name}</TableCell>
|
||||
<PluginChannelConfigurationCell plugin={plugin} />
|
||||
<PluginChannelAvailabilityCell plugin={plugin} />
|
||||
<TableCell align="right">
|
||||
<div onClick={plugin ? onRowClick(plugin.id) : undefined}>
|
||||
<EditIcon />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={totalColSpan}>
|
||||
<Skeleton />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={classes.colAction}>
|
||||
<div onClick={plugin ? onRowClick(plugin.id) : undefined}>
|
||||
<EditIcon />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
() => (
|
||||
<TableRow>
|
||||
<TableCell colSpan={numberOfColumns}>
|
||||
<TableCell colSpan={totalColSpan}>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "No plugins found"
|
||||
})}
|
||||
|
|
56
src/plugins/components/PluginsList/messages.ts
Normal file
56
src/plugins/components/PluginsList/messages.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const pluginAvailabilityStatusMessages = defineMessages({
|
||||
channelTitle: {
|
||||
defaultMessage: "Active in {activeChannelsCount}",
|
||||
description: "plugin channel availability status title"
|
||||
}
|
||||
});
|
||||
|
||||
export const channelConfigPluginMessages = defineMessages({
|
||||
title: {
|
||||
defaultMessage:
|
||||
"Assigned to {activeChannelsCount} of {allChannelsCount} channels",
|
||||
description: "channel config plugin status popup title"
|
||||
}
|
||||
});
|
||||
|
||||
export const globalConfigPluginMessages = defineMessages({
|
||||
title: {
|
||||
defaultMessage: "Global Plugin",
|
||||
description: "global config plugin status popup title",
|
||||
id: "globalConfigPluginMessages title"
|
||||
},
|
||||
description: {
|
||||
defaultMessage:
|
||||
"Global plugins are set across all channels in your ecommerce. Only status is shown for those types of plugins",
|
||||
description: "global config plugin status popup description"
|
||||
}
|
||||
});
|
||||
|
||||
export const pluginsListTableHeadMessages = defineMessages({
|
||||
nameLabel: {
|
||||
defaultMessage: "Name",
|
||||
description: "table header name col label"
|
||||
},
|
||||
confLabel: {
|
||||
defaultMessage: "Configuration",
|
||||
description: "table header configuration col label"
|
||||
},
|
||||
channelLabel: {
|
||||
defaultMessage: "Channel",
|
||||
description: "table header channel col label"
|
||||
}
|
||||
});
|
||||
|
||||
export const pluginChannelConfigurationCellMessages = defineMessages({
|
||||
globalLabel: {
|
||||
defaultMessage: "Global",
|
||||
description: "PluginChannelConfigurationCell global title"
|
||||
},
|
||||
channelLabel: {
|
||||
defaultMessage: "Per channel",
|
||||
description: "PluginChannelConfigurationCell channel title",
|
||||
id: "pluginChannelConfigurationCellMessages per channel"
|
||||
}
|
||||
});
|
9
src/plugins/components/PluginsList/utils.ts
Normal file
9
src/plugins/components/PluginsList/utils.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Plugin_plugin_channelConfigurations } from "@saleor/plugins/types/Plugin";
|
||||
|
||||
export const getAllChannelConfigsCount = (
|
||||
channelConfigurations: Plugin_plugin_channelConfigurations[]
|
||||
) => channelConfigurations?.length;
|
||||
|
||||
export const getActiveChannelConfigsCount = (
|
||||
channelConfigurations: Plugin_plugin_channelConfigurations[]
|
||||
) => channelConfigurations?.filter(({ active }) => !!active).length;
|
|
@ -21,6 +21,7 @@ import {
|
|||
PluginFilterKeys,
|
||||
PluginListFilterOpts
|
||||
} from "./filters";
|
||||
import { pluginsFilterErrorMessages } from "./messages";
|
||||
|
||||
export interface PluginsListPageProps
|
||||
extends PageListProps,
|
||||
|
@ -58,6 +59,7 @@ const PluginsListPage: React.FC<PluginsListPageProps> = ({
|
|||
<PageHeader title={intl.formatMessage(sectionNames.plugins)} />
|
||||
<Card>
|
||||
<FilterBar
|
||||
errorMessages={pluginsFilterErrorMessages}
|
||||
currentTab={currentTab}
|
||||
initialSearch={initialSearch}
|
||||
onAll={onAll}
|
||||
|
|
|
@ -1,25 +1,43 @@
|
|||
import { IFilter } from "@saleor/components/Filter";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { FilterOpts } from "@saleor/types";
|
||||
import { createBooleanField } from "@saleor/utils/filters/fields";
|
||||
import { IFilter, IFilterElement } from "@saleor/components/Filter";
|
||||
import { statusLabelMessages } from "@saleor/components/StatusLabel/messages";
|
||||
import { sectionNames } from "@saleor/intl";
|
||||
import { AutocompleteFilterOpts, FilterOpts } from "@saleor/types";
|
||||
import { PluginConfigurationType } from "@saleor/types/globalTypes";
|
||||
import {
|
||||
createAutocompleteField,
|
||||
createBooleanField,
|
||||
createOptionsField
|
||||
} from "@saleor/utils/filters/fields";
|
||||
import { defineMessages, IntlShape } from "react-intl";
|
||||
|
||||
import { pluginChannelConfigurationCellMessages } from "../PluginsList/messages";
|
||||
|
||||
export enum PluginFilterKeys {
|
||||
active = "active"
|
||||
active = "active",
|
||||
channels = "channels",
|
||||
status = "status",
|
||||
type = "type"
|
||||
}
|
||||
|
||||
export interface PluginListFilterOpts {
|
||||
isActive: FilterOpts<boolean>;
|
||||
channels: FilterOpts<string[]> & AutocompleteFilterOpts;
|
||||
type: FilterOpts<PluginConfigurationType>;
|
||||
status: FilterOpts<boolean>;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
active: {
|
||||
defaultMessage: "Active",
|
||||
description: "plugin"
|
||||
channelStatusSectionTitle: {
|
||||
defaultMessage: "Status in channel",
|
||||
description: "status section title"
|
||||
},
|
||||
deactivated: {
|
||||
defaultMessage: "Inactive",
|
||||
description: "plugin"
|
||||
channelStatusSectionSubtitle: {
|
||||
defaultMessage: "Channel status",
|
||||
description: "status section subtitle"
|
||||
},
|
||||
configTypeSectionTitle: {
|
||||
defaultMessage: "Configuration Type",
|
||||
description: "config type section title"
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -29,16 +47,65 @@ export function createFilterStructure(
|
|||
): IFilter<PluginFilterKeys> {
|
||||
return [
|
||||
{
|
||||
...createBooleanField(
|
||||
PluginFilterKeys.active,
|
||||
intl.formatMessage(commonMessages.status),
|
||||
opts.isActive.value,
|
||||
active: opts.status.active,
|
||||
name: PluginFilterKeys.status,
|
||||
label: intl.formatMessage(messages.channelStatusSectionTitle),
|
||||
multipleFields: [
|
||||
{
|
||||
negative: intl.formatMessage(messages.deactivated),
|
||||
positive: intl.formatMessage(messages.active)
|
||||
required: true,
|
||||
...createBooleanField(
|
||||
PluginFilterKeys.active,
|
||||
intl.formatMessage(messages.channelStatusSectionSubtitle),
|
||||
opts.isActive.value,
|
||||
{
|
||||
negative: intl.formatMessage(statusLabelMessages.inactive),
|
||||
positive: intl.formatMessage(statusLabelMessages.active)
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
...createAutocompleteField(
|
||||
PluginFilterKeys.channels,
|
||||
intl.formatMessage(sectionNames.channels),
|
||||
opts.channels.value,
|
||||
opts.channels.displayValues,
|
||||
true,
|
||||
opts.channels.choices,
|
||||
{
|
||||
hasMore: opts.channels.hasMore,
|
||||
initialSearch: "",
|
||||
loading: opts.channels.loading,
|
||||
onFetchMore: opts.channels.onFetchMore,
|
||||
onSearchChange: opts.channels.onSearchChange
|
||||
}
|
||||
)
|
||||
}
|
||||
),
|
||||
active: opts.isActive.active
|
||||
]
|
||||
} as IFilterElement<PluginFilterKeys.status>,
|
||||
{
|
||||
active: opts.type.active,
|
||||
name: PluginFilterKeys.type,
|
||||
...createOptionsField(
|
||||
PluginFilterKeys.type,
|
||||
intl.formatMessage(messages.configTypeSectionTitle),
|
||||
[],
|
||||
false,
|
||||
[
|
||||
{
|
||||
value: PluginConfigurationType.GLOBAL,
|
||||
label: intl.formatMessage(
|
||||
pluginChannelConfigurationCellMessages.globalLabel
|
||||
)
|
||||
},
|
||||
{
|
||||
value: PluginConfigurationType.PER_CHANNEL,
|
||||
label: intl.formatMessage(
|
||||
pluginChannelConfigurationCellMessages.channelLabel
|
||||
)
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
12
src/plugins/components/PluginsListPage/messages.ts
Normal file
12
src/plugins/components/PluginsListPage/messages.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const pluginsFilterErrorMessages = defineMessages({
|
||||
active: {
|
||||
defaultMessage: "Active is not selected",
|
||||
description: "plugin filters error messages active"
|
||||
},
|
||||
channels: {
|
||||
defaultMessage: "No channels selected",
|
||||
description: "plugin filters error messages channels"
|
||||
}
|
||||
});
|
|
@ -6,7 +6,20 @@ import { Plugins_plugins_edges_node } from "./types/Plugins";
|
|||
export const pluginList: Plugins_plugins_edges_node[] = [
|
||||
{
|
||||
__typename: "Plugin",
|
||||
active: true,
|
||||
globalConfiguration: null,
|
||||
channelConfigurations: [
|
||||
{
|
||||
__typename: "PluginConfiguration",
|
||||
active: true,
|
||||
configuration: [],
|
||||
channel: {
|
||||
__typename: "Channel",
|
||||
id: "channel-1",
|
||||
name: "channel 1",
|
||||
slug: "channel-1"
|
||||
}
|
||||
}
|
||||
],
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna.",
|
||||
id: "Jzx123sEt==",
|
||||
|
@ -14,7 +27,20 @@ export const pluginList: Plugins_plugins_edges_node[] = [
|
|||
},
|
||||
{
|
||||
__typename: "Plugin",
|
||||
active: false,
|
||||
globalConfiguration: null,
|
||||
channelConfigurations: [
|
||||
{
|
||||
__typename: "PluginConfiguration",
|
||||
active: true,
|
||||
configuration: [],
|
||||
channel: {
|
||||
__typename: "Channel",
|
||||
id: "channel-1",
|
||||
name: "channel 1",
|
||||
slug: "channel-1"
|
||||
}
|
||||
}
|
||||
],
|
||||
description:
|
||||
"Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna.",
|
||||
id: "Jzx123sEt==",
|
||||
|
@ -23,64 +49,76 @@ export const pluginList: Plugins_plugins_edges_node[] = [
|
|||
];
|
||||
export const plugin: Plugin_plugin = {
|
||||
__typename: "Plugin",
|
||||
active: true,
|
||||
configuration: [
|
||||
globalConfiguration: null,
|
||||
channelConfigurations: [
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "Provide user or account details",
|
||||
label: "Username or account",
|
||||
name: "Username or account",
|
||||
type: ConfigurationTypeFieldEnum.STRING,
|
||||
value: "avatax_user"
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "Provide password or license details",
|
||||
label: "Password or license",
|
||||
name: "Password or license",
|
||||
type: ConfigurationTypeFieldEnum.STRING,
|
||||
value: "TEM8S2-2ET83-CGKP1-DPSI2-EPZO1"
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "This key will enable you to connect to Avatax API",
|
||||
label: "API key",
|
||||
name: "apiKey",
|
||||
type: ConfigurationTypeFieldEnum.SECRET,
|
||||
value: "9ab9"
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "",
|
||||
label: "Password",
|
||||
name: "password",
|
||||
type: ConfigurationTypeFieldEnum.PASSWORD,
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "",
|
||||
label: "Empty Password",
|
||||
name: "password-not-set",
|
||||
type: ConfigurationTypeFieldEnum.PASSWORD,
|
||||
value: null
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "Determines if Saleor should use Avatax sandbox API.",
|
||||
label: "Use sandbox",
|
||||
name: "Use sandbox",
|
||||
type: ConfigurationTypeFieldEnum.BOOLEAN,
|
||||
value: "true"
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "This is a multiline field",
|
||||
label: "Multiline Field",
|
||||
name: "multiline-field",
|
||||
type: ConfigurationTypeFieldEnum.MULTILINE,
|
||||
value:
|
||||
"Lorem ipsum\ndolor sit\namet enim.\nEtiam ullamcorper.\nSuspendisse a\npellentesque dui,\nnon felis."
|
||||
__typename: "PluginConfiguration",
|
||||
active: true,
|
||||
channel: {
|
||||
__typename: "Channel",
|
||||
id: "channel-1",
|
||||
name: "channel 1",
|
||||
slug: "channel-1"
|
||||
},
|
||||
configuration: [
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "Provide user or account details",
|
||||
label: "Username or account",
|
||||
name: "Username or account",
|
||||
type: ConfigurationTypeFieldEnum.STRING,
|
||||
value: "avatax_user"
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "Provide password or license details",
|
||||
label: "Password or license",
|
||||
name: "Password or license",
|
||||
type: ConfigurationTypeFieldEnum.STRING,
|
||||
value: "TEM8S2-2ET83-CGKP1-DPSI2-EPZO1"
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "This key will enable you to connect to Avatax API",
|
||||
label: "API key",
|
||||
name: "apiKey",
|
||||
type: ConfigurationTypeFieldEnum.SECRET,
|
||||
value: "9ab9"
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "",
|
||||
label: "Password",
|
||||
name: "password",
|
||||
type: ConfigurationTypeFieldEnum.PASSWORD,
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "",
|
||||
label: "Empty Password",
|
||||
name: "password-not-set",
|
||||
type: ConfigurationTypeFieldEnum.PASSWORD,
|
||||
value: null
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "Determines if Saleor should use Avatax sandbox API.",
|
||||
label: "Use sandbox",
|
||||
name: "Use sandbox",
|
||||
type: ConfigurationTypeFieldEnum.BOOLEAN,
|
||||
value: "true"
|
||||
},
|
||||
{
|
||||
__typename: "ConfigurationItem",
|
||||
helpText: "This is a multiline field",
|
||||
label: "Multiline Field",
|
||||
name: "multiline-field",
|
||||
type: ConfigurationTypeFieldEnum.MULTILINE,
|
||||
value:
|
||||
"Lorem ipsum\ndolor sit\namet enim.\nEtiam ullamcorper.\nSuspendisse a\npellentesque dui,\nnon felis."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
description:
|
||||
|
|
|
@ -8,8 +8,8 @@ import { PluginUpdate, PluginUpdateVariables } from "./types/PluginUpdate";
|
|||
const pluginUpdate = gql`
|
||||
${pluginsDetailsFragment}
|
||||
${pluginErrorFragment}
|
||||
mutation PluginUpdate($id: ID!, $input: PluginUpdateInput!) {
|
||||
pluginUpdate(id: $id, input: $input) {
|
||||
mutation PluginUpdate($channel: ID!, $id: ID!, $input: PluginUpdateInput!) {
|
||||
pluginUpdate(channel: $channel, id: $id, input: $input) {
|
||||
errors {
|
||||
...PluginErrorFragment
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
import makeQuery from "@saleor/hooks/makeQuery";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { TypedQuery } from "../queries";
|
||||
import { Plugin, PluginVariables } from "./types/Plugin";
|
||||
import { Plugins, PluginsVariables } from "./types/Plugins";
|
||||
|
||||
|
@ -53,6 +52,7 @@ const pluginsDetails = gql`
|
|||
}
|
||||
}
|
||||
`;
|
||||
export const TypedPluginsDetailsQuery = TypedQuery<Plugin, PluginVariables>(
|
||||
|
||||
export const usePluginDetails = makeQuery<Plugin, PluginVariables>(
|
||||
pluginsDetails
|
||||
);
|
||||
|
|
|
@ -9,22 +9,59 @@ import { ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
|
|||
// GraphQL query operation: Plugin
|
||||
// ====================================================
|
||||
|
||||
export interface Plugin_plugin_configuration {
|
||||
export interface Plugin_plugin_globalConfiguration_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface Plugin_plugin_globalConfiguration_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface Plugin_plugin_globalConfiguration {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: Plugin_plugin_globalConfiguration_channel | null;
|
||||
configuration: (Plugin_plugin_globalConfiguration_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface Plugin_plugin_channelConfigurations_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface Plugin_plugin_channelConfigurations_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface Plugin_plugin_channelConfigurations {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: Plugin_plugin_channelConfigurations_channel | null;
|
||||
configuration: (Plugin_plugin_channelConfigurations_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface Plugin_plugin {
|
||||
__typename: "Plugin";
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
configuration: (Plugin_plugin_configuration | null)[] | null;
|
||||
globalConfiguration: Plugin_plugin_globalConfiguration | null;
|
||||
channelConfigurations: Plugin_plugin_channelConfigurations[];
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
|
|
|
@ -15,22 +15,59 @@ export interface PluginUpdate_pluginUpdate_errors {
|
|||
field: string | null;
|
||||
}
|
||||
|
||||
export interface PluginUpdate_pluginUpdate_plugin_configuration {
|
||||
export interface PluginUpdate_pluginUpdate_plugin_globalConfiguration_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface PluginUpdate_pluginUpdate_plugin_globalConfiguration_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface PluginUpdate_pluginUpdate_plugin_globalConfiguration {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: PluginUpdate_pluginUpdate_plugin_globalConfiguration_channel | null;
|
||||
configuration: (PluginUpdate_pluginUpdate_plugin_globalConfiguration_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface PluginUpdate_pluginUpdate_plugin_channelConfigurations_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface PluginUpdate_pluginUpdate_plugin_channelConfigurations_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface PluginUpdate_pluginUpdate_plugin_channelConfigurations {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: PluginUpdate_pluginUpdate_plugin_channelConfigurations_channel | null;
|
||||
configuration: (PluginUpdate_pluginUpdate_plugin_channelConfigurations_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface PluginUpdate_pluginUpdate_plugin {
|
||||
__typename: "Plugin";
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
configuration: (PluginUpdate_pluginUpdate_plugin_configuration | null)[] | null;
|
||||
globalConfiguration: PluginUpdate_pluginUpdate_plugin_globalConfiguration | null;
|
||||
channelConfigurations: PluginUpdate_pluginUpdate_plugin_channelConfigurations[];
|
||||
}
|
||||
|
||||
export interface PluginUpdate_pluginUpdate {
|
||||
|
@ -44,6 +81,7 @@ export interface PluginUpdate {
|
|||
}
|
||||
|
||||
export interface PluginUpdateVariables {
|
||||
channel: string;
|
||||
id: string;
|
||||
input: PluginUpdateInput;
|
||||
}
|
||||
|
|
|
@ -3,18 +3,65 @@
|
|||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { PluginFilterInput, PluginSortingInput } from "./../../types/globalTypes";
|
||||
import { PluginFilterInput, PluginSortingInput, ConfigurationTypeFieldEnum } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: Plugins
|
||||
// ====================================================
|
||||
|
||||
export interface Plugins_plugins_edges_node_globalConfiguration_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface Plugins_plugins_edges_node_globalConfiguration_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface Plugins_plugins_edges_node_globalConfiguration {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: Plugins_plugins_edges_node_globalConfiguration_channel | null;
|
||||
configuration: (Plugins_plugins_edges_node_globalConfiguration_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface Plugins_plugins_edges_node_channelConfigurations_channel {
|
||||
__typename: "Channel";
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface Plugins_plugins_edges_node_channelConfigurations_configuration {
|
||||
__typename: "ConfigurationItem";
|
||||
name: string;
|
||||
value: string | null;
|
||||
type: ConfigurationTypeFieldEnum | null;
|
||||
helpText: string | null;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
export interface Plugins_plugins_edges_node_channelConfigurations {
|
||||
__typename: "PluginConfiguration";
|
||||
active: boolean;
|
||||
channel: Plugins_plugins_edges_node_channelConfigurations_channel | null;
|
||||
configuration: (Plugins_plugins_edges_node_channelConfigurations_configuration | null)[] | null;
|
||||
}
|
||||
|
||||
export interface Plugins_plugins_edges_node {
|
||||
__typename: "Plugin";
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
globalConfiguration: Plugins_plugins_edges_node_globalConfiguration | null;
|
||||
channelConfigurations: Plugins_plugins_edges_node_channelConfigurations[];
|
||||
}
|
||||
|
||||
export interface Plugins_plugins_edges {
|
||||
|
|
|
@ -14,11 +14,17 @@ import {
|
|||
export const pluginSection = "/plugins/";
|
||||
|
||||
export const pluginListPath = pluginSection;
|
||||
|
||||
export enum PluginListUrlFiltersEnum {
|
||||
query = "query",
|
||||
active = "active",
|
||||
query = "query"
|
||||
channels = "channels",
|
||||
type = "type"
|
||||
}
|
||||
export type PluginListUrlFilters = Filters<PluginListUrlFiltersEnum>;
|
||||
|
||||
export type PluginListUrlFilters = Filters<PluginListUrlFiltersEnum> & {
|
||||
channels?: string[];
|
||||
};
|
||||
export type PluginListUrlDialog = TabActionDialog;
|
||||
export enum PluginListUrlSortField {
|
||||
name = "name",
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { PluginConfigurationFragment_configuration } from "@saleor/fragments/types/PluginConfigurationFragment";
|
||||
import { ConfigurationTypeFieldEnum } from "@saleor/types/globalTypes";
|
||||
|
||||
import { Plugin_plugin_configuration } from "./types/Plugin";
|
||||
|
||||
export function isSecretField(
|
||||
config: Plugin_plugin_configuration[],
|
||||
config: PluginConfigurationFragment_configuration[],
|
||||
field: string
|
||||
) {
|
||||
return [
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { PluginConfigurationFragment_configuration } from "@saleor/fragments/types/PluginConfigurationFragment";
|
||||
|
||||
import {
|
||||
ConfigurationItemInput,
|
||||
ConfigurationTypeFieldEnum
|
||||
} from "../../types/globalTypes";
|
||||
import { Plugin_plugin_configuration } from "../types/Plugin";
|
||||
import { getConfigurationInput } from "./PluginsDetails";
|
||||
|
||||
const baseConfig: Omit<
|
||||
Plugin_plugin_configuration,
|
||||
PluginConfigurationFragment_configuration,
|
||||
"name" | "type" | "value"
|
||||
> = {
|
||||
__typename: "ConfigurationItem",
|
||||
|
@ -14,7 +15,7 @@ const baseConfig: Omit<
|
|||
label: ""
|
||||
};
|
||||
|
||||
const config: Plugin_plugin_configuration[] = [
|
||||
const config: PluginConfigurationFragment_configuration[] = [
|
||||
{
|
||||
...baseConfig,
|
||||
name: "field-1",
|
||||
|
|
|
@ -3,6 +3,7 @@ import SaveFilterTabDialog, {
|
|||
SaveFilterTabDialogFormData
|
||||
} from "@saleor/components/SaveFilterTabDialog";
|
||||
import { configurationMenuUrl } from "@saleor/configuration";
|
||||
import { useChannelsSearchWithLoadMore } from "@saleor/hooks/useChannelsSearchWithLoadMore";
|
||||
import useListSettings from "@saleor/hooks/useListSettings";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import usePaginator, {
|
||||
|
@ -113,13 +114,16 @@ export const PluginsList: React.FC<PluginsListProps> = ({ params }) => {
|
|||
);
|
||||
|
||||
const handleSort = createSortHandler(navigate, pluginListUrl, params);
|
||||
const channelsSearchWithLoadMoreProps = useChannelsSearchWithLoadMore();
|
||||
|
||||
const filterOpts = getFilterOpts(params, channelsSearchWithLoadMoreProps);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PluginsListPage
|
||||
currentTab={currentTab}
|
||||
disabled={loading}
|
||||
filterOpts={getFilterOpts(params)}
|
||||
filterOpts={filterOpts}
|
||||
initialSearch={params.query || ""}
|
||||
settings={settings}
|
||||
plugins={mapEdgesToItems(data?.plugins)}
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
exports[`Filtering URL params should not be empty if active filters are present 1`] = `
|
||||
Object {
|
||||
"active": "true",
|
||||
"type": "GLOBAL",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"active=true"`;
|
||||
exports[`Filtering URL params should not be empty if active filters are present 2`] = `"active=true&type=GLOBAL"`;
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { createFilterStructure } from "@saleor/plugins/components/PluginsListPage";
|
||||
import {
|
||||
createFilterStructure,
|
||||
PluginFilterKeys
|
||||
} from "@saleor/plugins/components/PluginsListPage";
|
||||
import { PluginListUrlFilters } from "@saleor/plugins/urls";
|
||||
import { PluginConfigurationType } from "@saleor/types/globalTypes";
|
||||
import { getFilterQueryParams } from "@saleor/utils/filters";
|
||||
import { getExistingKeys, setFilterOptsStatus } from "@test/filters";
|
||||
import { getExistingKeys } from "@test/filters";
|
||||
import { config } from "@test/intl";
|
||||
import { stringify as stringifyQs } from "qs";
|
||||
import { createIntl } from "react-intl";
|
||||
|
@ -18,7 +22,7 @@ describe("Filtering query params", () => {
|
|||
|
||||
it("should not be empty object if params given", () => {
|
||||
const params: PluginListUrlFilters = {
|
||||
active: true.toString()
|
||||
type: PluginConfigurationType.GLOBAL
|
||||
};
|
||||
const filterVariables = getFilterVariables(params);
|
||||
|
||||
|
@ -33,6 +37,25 @@ describe("Filtering URL params", () => {
|
|||
isActive: {
|
||||
active: false,
|
||||
value: true
|
||||
},
|
||||
channels: {
|
||||
active: false,
|
||||
choices: [],
|
||||
displayValues: [],
|
||||
initialSearch: "",
|
||||
hasMore: false,
|
||||
loading: false,
|
||||
onFetchMore: () => undefined,
|
||||
onSearchChange: () => undefined,
|
||||
value: []
|
||||
},
|
||||
status: {
|
||||
active: false,
|
||||
value: false
|
||||
},
|
||||
type: {
|
||||
active: false,
|
||||
value: PluginConfigurationType.GLOBAL
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -47,7 +70,22 @@ describe("Filtering URL params", () => {
|
|||
|
||||
it("should not be empty if active filters are present", () => {
|
||||
const filterQueryParams = getFilterQueryParams(
|
||||
setFilterOptsStatus(filters, true),
|
||||
[
|
||||
{
|
||||
name: PluginFilterKeys.active,
|
||||
label: "Active",
|
||||
multiple: false,
|
||||
active: true,
|
||||
value: ["true"]
|
||||
},
|
||||
{
|
||||
name: PluginFilterKeys.type,
|
||||
label: "Configuration type",
|
||||
multiple: false,
|
||||
active: true,
|
||||
value: [PluginConfigurationType.GLOBAL]
|
||||
}
|
||||
],
|
||||
getFilterQueryParam
|
||||
);
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue