Feature - channels per plugin (#1093)

* Update schema

* Update queries, mutations, and types

* Add render with dividers util function

* Add plugin details channels card component

* Update plugin details to use channels

* Update stories

* Update plugin configuration type across the app, fix some other types, temporarily comment some things out in plugins list so types match"

* Update schema

* Update types

* Update plugins list to show channels and global statuses, add plugin channel status, update status label component

* Add render with dividers util function

* Comment out some stuff for types to match - temporary

* Add useChannelsSearchWithLoadMore util to imitate loading more from backend for channels list with load more

* Change filters logic to be able to display multiple fields in a field section and add it to plugins view

* Add scroll option to plugin availability popup on plugin list

* Fix plugin list page story

* Temporarily comment some stuff out, fix some types

* Add filters errors WIP

* Fix filters not updating list

* Add error handling to plugins list filters and filters in general

* Rename some components according to review

* Move useChannelsSearch and useChannelsSearchWithLoadMore to hooks, change some imports accordingly

* Fix imports

* Move render collection with dividers to a component, fix usages

* Replace channels with load more and search query to base channels query

* Change render with dividers function to take in a component instead of render function

* Update tests

* Extract messages

* Remove unnecessary imports

* Fix filters - autocomplete messing items order sometimes & some fields not working

* Update plugin update mutation variables - change channelId to channel

* fix failing tests

* Add test ids

* fix failing tests

* fix failing tests

* Rename misc.tsx to ts

* Remove usage of render collection with diviers, change it to CollectionWithDividers component

* Remove unnecessary imports

* Update messages ids

* Update snapshots

Co-authored-by: Karolina Rakoczy <rakoczy.karolina@gmail.com>
This commit is contained in:
mmarkusik 2021-05-11 15:58:09 +02:00 committed by GitHub
parent bb441ea11a
commit 19b8d30565
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 7496 additions and 2894 deletions

View file

@ -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

View file

@ -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"]'
};

View 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"]'
};

View file

@ -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"
}
}

View file

@ -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 => {

View file

@ -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 => {

View file

@ -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();

View file

@ -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");
}

View 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();
}

View file

@ -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,

View file

@ -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;

View file

@ -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 wont 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 wont 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 wont 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 wont 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 wont 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 wont 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": "Plugins 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"
},

View file

@ -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
@ -3813,6 +3813,7 @@ enum PaymentErrorCode {
SHIPPING_METHOD_NOT_SET
PAYMENT_ERROR
NOT_SUPPORTED_GATEWAY
CHANNEL_INACTIVE
}
input PaymentFilterInput {
@ -3951,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!]!
@ -3986,8 +3998,9 @@ enum PluginErrorCode {
}
input PluginFilterInput {
active: Boolean
statusInChannels: PluginStatusInChannelsInput
search: String
type: PluginConfigurationType
}
enum PluginSortField {
@ -4000,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.")

View file

@ -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 };
};

View file

@ -202,6 +202,7 @@ const AddressEdit: React.FC<AddressEditProps> = props => {
<div>
<SingleAutocompleteSelectField
disabled={disabled}
data-test-id="address-edit-country-select-field"
displayValue={countryDisplayValue}
error={!!formErrors.country}
helperText={getErrorMessage(formErrors.country, intl)}

View 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;

View file

@ -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;

View file

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

View file

@ -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>
)}

View file

@ -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}

View file

@ -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;

View 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;

View 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;

View file

@ -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;

View 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;

View 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;

View file

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

View 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
}
];
}

View file

@ -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 {

View file

@ -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"
});

View 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);
},
[]
);
};

View file

@ -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}

View file

@ -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;

View 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"
}
});

View file

@ -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
}
}
`;

View 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;
}

View 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;
}

View 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;
}

View file

@ -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[];
}

View file

@ -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[];
}

View 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[];
}

View 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
};
};

View file

@ -108,6 +108,7 @@ const OrderAddressEditDialog: React.FC<OrderAddressEditDialogProps> = props => {
<FormattedMessage {...buttonMessages.back} />
</Button>
<ConfirmButton
data-test-id="order-address-edit-dialog-confirm-button"
transitionState={confirmButtonState}
color="primary"
variant="contained"

View file

@ -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}

View file

@ -4,12 +4,12 @@ export const baseMessages = defineMessages({
title: {
defaultMessage:
"Delete page {selectedTypesCount,plural,one{type} other{types}}",
description: "ProductTypeDeleteWarningDialog title"
description: "PageTypeDeleteWarningDialog title"
},
viewAssignedItemsButtonLabel: {
defaultMessage: "View pages",
description:
"ProductTypeDeleteWarningDialog single assigned items button label"
"PageTypeDeleteWarningDialog single assigned items button label"
}
});
@ -18,11 +18,13 @@ export const singleWithItemsMessages = defineMessages({
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:
"ProductTypeDeleteWarningDialog single assigned items 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: "ProductTypeDeleteWarningDialog single consent label"
description: "PageTypeDeleteWarningDialog single consent label",
id: "PageTypeDeleteWarningDialog single consent label"
}
});
@ -30,13 +32,14 @@ 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:
"ProductTypeDeleteWarningDialog with items multiple description"
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: "ProductTypeDeleteWarningDialog multiple consent label"
description: "PageTypeDeleteWarningDialog multiple consent label",
id: "PageTypeDeleteWarningDialog multiple consent label"
}
});
@ -45,7 +48,9 @@ export const singleWithoutItemsMessages = defineMessages({
defaultMessage:
"Are you sure you want to delete <b>{typeName}</b>? If you remove it you wont be able to assign it to created pages.",
description:
"ProductTypeDeleteWarningDialog single assigned items description"
"PageTypeDeleteWarningDialog single no assigned items description",
id: "PageTypeDeleteWarningDialog single no assigned items description"
}
});
@ -54,6 +59,8 @@ export const multipleWithoutItemsMessages = defineMessages({
defaultMessage:
"Are you sure you want to delete selected page types? If you remove them you wont be able to assign them to created pages.",
description:
"ProductTypeDeleteWarningDialog single assigned items description"
"PageTypeDeleteWarningDialog multiple assigned items description",
id: "PageTypeDeleteWarningDialog multiple assigned items description"
}
});

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

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

View file

@ -0,0 +1,8 @@
import { defineMessages } from "react-intl";
export const pluginDetailsChannelsCardMessages = defineMessages({
noChannelsSubtitle: {
defaultMessage: "Plugins settings are set common across all channels",
description: "PluginDetailsChannelsCard no channels subtitle"
}
});

View file

@ -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;
}

View file

@ -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(

View file

@ -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;

View file

@ -0,0 +1,3 @@
import { PluginConfigurationFragment } from "@saleor/fragments/types/PluginConfigurationFragment";
export type PluginConfiguration = PluginConfigurationFragment;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

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

View file

@ -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;

View file

@ -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;

View 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;

View file

@ -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"
})}

View 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"
}
});

View 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;

View file

@ -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}

View file

@ -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
)
}
]
)
}
];
}

View 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"
}
});

View file

@ -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:

View file

@ -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
}

View file

@ -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
);

View file

@ -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 {

View file

@ -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;
}

View file

@ -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 {

View file

@ -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",

View file

@ -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 [

View file

@ -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",

View file

@ -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, {
@ -112,13 +113,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={maybe(() => data.plugins.edges.map(edge => edge.node))}

View file

@ -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"`;

View file

@ -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
);

View file

@ -1,14 +1,22 @@
import { IFilterElement } from "@saleor/components/Filter";
import { ChannelsWithLoadMoreProps } from "@saleor/hooks/useChannelsSearch";
import { maybe, parseBoolean } from "@saleor/misc";
import {
PluginFilterKeys,
PluginListFilterOpts
} from "@saleor/plugins/components/PluginsListPage";
import { PluginFilterInput } from "@saleor/types/globalTypes";
import {
PluginConfigurationType,
PluginFilterInput
} from "@saleor/types/globalTypes";
import { mapNodeToChoice } from "@saleor/utils/maps";
import {
createFilterTabUtils,
createFilterUtils,
dedupeFilter,
getMultipleValueQueryParam,
getSingleEnumValueQueryParam,
getSingleValueQueryParam
} from "../../../utils/filters";
import {
@ -20,27 +28,67 @@ import {
export const PLUGIN_FILTERS_KEY = "pluginFilters";
export function getFilterOpts(
params: PluginListUrlFilters
params: PluginListUrlFilters,
{
channels,
hasMore,
onFetchMore,
onSearchChange,
loading
}: ChannelsWithLoadMoreProps
): PluginListFilterOpts {
return {
isActive: {
active: maybe(() => params.active !== undefined, false),
value:
params.active !== undefined ? parseBoolean(params.active, true) : true
params.active === undefined
? undefined
: parseBoolean(params.active, true)
},
channels: {
active: !!params.channels,
choices: mapNodeToChoice(channels),
displayValues: mapNodeToChoice(channels),
initialSearch: "",
hasMore,
loading,
onFetchMore,
onSearchChange,
value: maybe(() => dedupeFilter(params.channels), [])
},
type: {
active: !!params.type,
value: getParsedConfigType(params.type)
},
status: {
active: !!params.channels?.length && params.active !== undefined,
value:
!!dedupeFilter(params.channels)?.length && params.active !== undefined
}
};
}
const getParsedConfigType = (configTypeString?: string) =>
PluginConfigurationType[configTypeString] || undefined;
export function getFilterVariables(
params: PluginListUrlFilters
): PluginFilterInput {
return {
active:
params.active !== undefined
? parseBoolean(params.active, true)
: undefined,
search: params.query
const baseParams = {
type: getParsedConfigType(params.type)
};
if (!!params.active && !!params.channels?.length) {
return {
...baseParams,
statusInChannels: {
active: parseBoolean(params.active, true),
channels: params.channels
}
};
}
return baseParams;
}
export function getFilterQueryParam(
@ -49,8 +97,21 @@ export function getFilterQueryParam(
const { name } = filter;
switch (name) {
case PluginFilterKeys.channels:
return getMultipleValueQueryParam(
filter,
PluginListUrlFiltersEnum.channels
);
case PluginFilterKeys.active:
return getSingleValueQueryParam(filter, PluginListUrlFiltersEnum.active);
case PluginFilterKeys.type:
return getSingleEnumValueQueryParam(
filter,
PluginListUrlFiltersEnum.type,
PluginConfigurationType
);
}
}

View file

@ -1,8 +1,10 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import ActionDialog from "@saleor/components/ActionDialog";
import { WindowTitle } from "@saleor/components/WindowTitle";
import { PluginConfigurationFragment_configuration } from "@saleor/fragments/types/PluginConfigurationFragment";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import useStateFromProps from "@saleor/hooks/useStateFromProps";
import { commonMessages } from "@saleor/intl";
import { ConfigurationItemInput } from "@saleor/types/globalTypes";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
@ -11,11 +13,10 @@ import { FormattedMessage, useIntl } from "react-intl";
import PluginsDetailsPage, {
PluginDetailsPageFormData
} from "../components/PluginsDetailsPage";
} from "../components/PluginsDetailsPage"; // PluginDetailsPageFormData
import PluginSecretFieldDialog from "../components/PluginSecretFieldDialog";
import { TypedPluginUpdate } from "../mutations";
import { TypedPluginsDetailsQuery } from "../queries";
import { Plugin_plugin_configuration } from "../types/Plugin";
import { usePluginDetails } from "../queries";
import { PluginUpdate } from "../types/PluginUpdate";
import {
pluginListUrl,
@ -24,6 +25,8 @@ import {
PluginUrlQueryParams
} from "../urls";
import { isSecretField } from "../utils";
import { isPluginGlobal } from "./utils";
import { getConfigByChannelId } from "./utils";
export interface PluginsDetailsProps {
id: string;
@ -31,7 +34,7 @@ export interface PluginsDetailsProps {
}
export function getConfigurationInput(
config: Plugin_plugin_configuration[] | null,
config: PluginConfigurationFragment_configuration[] | null,
input: ConfigurationItemInput[] | null
): ConfigurationItemInput[] | null {
if (config === null || input === null) {
@ -54,6 +57,28 @@ export const PluginsDetails: React.FC<PluginsDetailsProps> = ({
const notify = useNotifier();
const intl = useIntl();
const { data: pluginData, loading } = usePluginDetails({
displayLoader: true,
variables: { id }
});
const plugin = pluginData?.plugin;
const initialSelectedChannelValue =
plugin && !isPluginGlobal(plugin.globalConfiguration)
? plugin.channelConfigurations[0].channel.id
: null;
const [selectedChannelId, setSelectedChannelId] = useStateFromProps(
initialSelectedChannelValue
);
const selectedConfig = isPluginGlobal(plugin?.globalConfiguration)
? plugin?.globalConfiguration
: plugin?.channelConfigurations.find(
getConfigByChannelId(selectedChannelId)
);
const [openModal, closeModal] = createDialogActionHandlers<
PluginUrlDialog,
PluginUrlQueryParams
@ -70,106 +95,104 @@ export const PluginsDetails: React.FC<PluginsDetailsProps> = ({
};
return (
<TypedPluginsDetailsQuery variables={{ id }}>
{pluginDetails => (
<TypedPluginUpdate onCompleted={handleUpdate}>
{(pluginUpdate, pluginUpdateOpts) => {
const formErrors = pluginUpdateOpts.data?.pluginUpdate.errors || [];
<TypedPluginUpdate onCompleted={handleUpdate}>
{(pluginUpdate, pluginUpdateOpts) => {
const formErrors = pluginUpdateOpts.data?.pluginUpdate.errors || [];
const handleFieldUpdate = (value: string) =>
pluginUpdate({
variables: {
id,
input: {
configuration: [
{
name: params.id,
value
}
]
const handleFieldUpdate = (value: string) =>
pluginUpdate({
variables: {
channel: selectedChannelId,
id,
input: {
configuration: [
{
name: params.id,
value
}
}
});
]
}
}
});
const handleSubmit = async (
formData: PluginDetailsPageFormData
) => {
const result = await pluginUpdate({
variables: {
id,
input: {
active: formData.active,
configuration: getConfigurationInput(
pluginDetails.data.plugin.configuration,
formData.configuration
)
}
}
});
const handleSubmit = async (formData: PluginDetailsPageFormData) => {
const result = await pluginUpdate({
variables: {
channel: selectedChannelId,
id,
input: {
active: formData.active,
configuration: getConfigurationInput(
selectedConfig?.configuration,
formData.configuration
)
}
}
});
return result.data.pluginUpdate.errors;
};
return result.data.pluginUpdate.errors;
};
return (
return (
<>
<WindowTitle title={plugin?.name} />
<PluginsDetailsPage
disabled={loading}
errors={formErrors}
saveButtonBarState={
!params.action ? pluginUpdateOpts.status : "default"
}
plugin={plugin}
onBack={() => navigate(pluginListUrl())}
onClear={id =>
openModal("clear", {
id
})
}
onEdit={id =>
openModal("edit", {
id
})
}
onSubmit={handleSubmit}
selectedConfig={selectedConfig}
setSelectedChannelId={setSelectedChannelId}
/>
{selectedConfig && (
<>
<WindowTitle title={pluginDetails.data?.plugin?.name} />
<PluginsDetailsPage
disabled={pluginDetails.loading}
errors={formErrors}
saveButtonBarState={
!params.action ? pluginUpdateOpts.status : "default"
<ActionDialog
confirmButtonState={
!!params.action ? pluginUpdateOpts.status : "default"
}
plugin={pluginDetails.data?.plugin}
onBack={() => navigate(pluginListUrl())}
onClear={id =>
openModal("clear", {
id
})
onClose={closeModal}
open={params.action === "clear" && !!params.id}
title={intl.formatMessage({
defaultMessage: "Authorization Field Delete",
description: "header"
})}
onConfirm={() => handleFieldUpdate(null)}
>
<DialogContentText>
<FormattedMessage defaultMessage="The plugin may stop working after this field is cleared. Are you sure you want to proceed?" />
</DialogContentText>
</ActionDialog>
<PluginSecretFieldDialog
confirmButtonState={
!!params.action ? pluginUpdateOpts.status : "default"
}
onEdit={id =>
openModal("edit", {
id
})
}
onSubmit={handleSubmit}
field={selectedConfig?.configuration.find(
field => field.name === params.id
)}
onClose={closeModal}
onConfirm={formData => handleFieldUpdate(formData.value)}
open={params.action === "edit" && !!params.id}
/>
{pluginDetails.data?.plugin?.configuration && (
<>
<ActionDialog
confirmButtonState={
!!params.action ? pluginUpdateOpts.status : "default"
}
onClose={closeModal}
open={params.action === "clear" && !!params.id}
title={intl.formatMessage({
defaultMessage: "Authorization Field Delete",
description: "header"
})}
onConfirm={() => handleFieldUpdate(null)}
>
<DialogContentText>
<FormattedMessage defaultMessage="The plugin may stop working after this field is cleared. Are you sure you want to proceed?" />
</DialogContentText>
</ActionDialog>
<PluginSecretFieldDialog
confirmButtonState={
!!params.action ? pluginUpdateOpts.status : "default"
}
field={pluginDetails.data?.plugin?.configuration.find(
field => field.name === params.id
)}
onClose={closeModal}
onConfirm={formData => handleFieldUpdate(formData.value)}
open={params.action === "edit" && !!params.id}
/>
</>
)}
</>
);
}}
</TypedPluginUpdate>
)}
</TypedPluginsDetailsQuery>
)}
</>
);
}}
</TypedPluginUpdate>
);
};
PluginsDetails.displayName = "PluginsDetails";

View file

@ -0,0 +1,11 @@
import { Plugin_plugin_globalConfiguration } from "@saleor/plugins/types/Plugin";
export const isPluginGlobal = (
globalConfiguration: Plugin_plugin_globalConfiguration
) => !!globalConfiguration;
export const getConfigByChannelId = (channelIdToCompare: string) => ({
channel
}: {
channel: { id: string };
}) => channel.id === channelIdToCompare;

View file

@ -18,12 +18,14 @@ export const singleWithItemsMessages = defineMessages({
defaultMessage:
"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?",
description:
"ProductTypeDeleteWarningDialog single assigned items description"
"ProductTypeDeleteWarningDialog single assigned items description",
id: "ProductTypeDeleteWarningDialog single assigned items description"
},
consentLabel: {
defaultMessage:
"Yes, I want to delete this product type and assigned products",
description: "ProductTypeDeleteWarningDialog single consent label"
description: "ProductTypeDeleteWarningDialog single consent label",
id: "ProductTypeDeleteWarningDialog single consent label"
}
});
@ -32,12 +34,14 @@ export const multipleWithItemsMessages = defineMessages({
defaultMessage:
"You are about to delete multiple product types. Some of them are assigned to products. Deleting those product types will also delete those products",
description:
"ProductTypeDeleteWarningDialog with items multiple description"
"ProductTypeDeleteWarningDialog with items multiple description",
id: "ProductTypeDeleteWarningDialog with items multiple description"
},
consentLabel: {
defaultMessage:
"Yes, I want to delete those products types and assigned products",
description: "ProductTypeDeleteWarningDialog multiple consent label"
description: "ProductTypeDeleteWarningDialog multiple consent label",
id: "ProductTypeDeleteWarningDialog multiple consent label"
}
});
@ -46,7 +50,8 @@ export const singleWithoutItemsMessages = defineMessages({
defaultMessage:
"Are you sure you want to delete <b>{typeName}</b>? If you remove it you wont be able to assign it to created products.",
description:
"ProductTypeDeleteWarningDialog single assigned items description"
"ProductTypeDeleteWarningDialog single no assigned items description",
id: "ProductTypeDeleteWarningDialog single no assigned items description"
}
});
@ -55,6 +60,7 @@ export const multipleWithoutItemsMessages = defineMessages({
defaultMessage:
"Are you sure you want to delete selected product types? If you remove them you wont be able to assign them to created products.",
description:
"ProductTypeDeleteWarningDialog single assigned items description"
"ProductTypeDeleteWarningDialog multiple assigned items description",
id: "ProductTypeDeleteWarningDialog multiple assigned items description"
}
});

View file

@ -1,9 +1,9 @@
import { useChannelsSearch } from "@saleor/channels/components/ChannelsAvailabilityDialog/utils";
import { Channels_channels } from "@saleor/channels/types/Channels";
import CardSpacer from "@saleor/components/CardSpacer";
import MultiAutocompleteSelectField, {
MultiAutocompleteChoiceType
} from "@saleor/components/MultiAutocompleteSelectField";
import { useChannelsSearch } from "@saleor/hooks/useChannelsSearch";
import { FormChange } from "@saleor/hooks/useForm";
import { mapNodeToChoice } from "@saleor/utils/maps";
import React from "react";

File diff suppressed because it is too large Load diff

View file

@ -10,21 +10,42 @@ import React from "react";
import Decorator from "../../Decorator";
const props: FilterContentProps = {
filters: [
createPriceField("price", "Price", {
max: "100.00",
min: "20.00"
const filtersData = [
createPriceField("price", "Price", {
max: "100.00",
min: "20.00"
}),
{
...createDateField("createdAt", "Created At", {
max: "2019-10-23",
min: "2019-09-09"
}),
{
...createDateField("createdAt", "Created At", {
max: "2019-10-23",
min: "2019-09-09"
}),
active: true
},
{
...createOptionsField("status", "Status", ["val1"], false, [
active: true
},
{
...createOptionsField("status", "Status", ["val1"], false, [
{
label: "Value 1",
value: "val1"
},
{
label: "Value 2",
value: "val2"
},
{
label: "Value 3",
value: "val3"
}
]),
active: true
},
{
...createOptionsField(
"multiplOptions",
"Multiple Options",
["val1", "val2"],
true,
[
{
label: "Value 1",
value: "val1"
@ -37,33 +58,15 @@ const props: FilterContentProps = {
label: "Value 3",
value: "val3"
}
]),
active: true
},
{
...createOptionsField(
"multiplOptions",
"Multiple Options",
["val1", "val2"],
true,
[
{
label: "Value 1",
value: "val1"
},
{
label: "Value 2",
value: "val2"
},
{
label: "Value 3",
value: "val3"
}
]
),
active: false
}
],
]
),
active: false
}
];
const props: FilterContentProps = {
filters: filtersData,
dataStructure: filtersData,
onClear: () => undefined,
onFilterPropertyChange: () => undefined,
onSubmit: () => undefined

View file

@ -17,7 +17,8 @@ const props: PluginsDetailsPageProps = {
onEdit: () => undefined,
onSubmit: () => undefined,
plugin,
saveButtonBarState: "default"
saveButtonBarState: "default",
setSelectedChannelId: () => undefined
};
storiesOf("Views / Plugins / Plugin details", module)
@ -49,8 +50,7 @@ storiesOf("Views / Plugins / Plugin details", module)
<PluginsDetailsPage
{...props}
plugin={{
...plugin,
configuration: null
...plugin
}}
/>
));

View file

@ -1,4 +1,5 @@
import { PluginListUrlSortField } from "@saleor/plugins/urls";
import { PluginConfigurationType } from "@saleor/types/globalTypes";
import { storiesOf } from "@storybook/react";
import React from "react";
@ -21,6 +22,25 @@ const props: PluginsListPageProps = {
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
}
},
onBack: () => undefined,

View file

@ -793,6 +793,11 @@ export enum PermissionGroupSortField {
NAME = "NAME",
}
export enum PluginConfigurationType {
GLOBAL = "GLOBAL",
PER_CHANNEL = "PER_CHANNEL",
}
export enum PluginErrorCode {
GRAPHQL_ERROR = "GRAPHQL_ERROR",
INVALID = "INVALID",
@ -1602,8 +1607,9 @@ export interface PermissionGroupUpdateInput {
}
export interface PluginFilterInput {
active?: boolean | null;
statusInChannels?: PluginStatusInChannelsInput | null;
search?: string | null;
type?: PluginConfigurationType | null;
}
export interface PluginSortingInput {
@ -1611,6 +1617,11 @@ export interface PluginSortingInput {
field: PluginSortField;
}
export interface PluginStatusInChannelsInput {
active: boolean;
channels: string[];
}
export interface PluginUpdateInput {
active?: boolean | null;
configuration?: (ConfigurationItemInput | null)[] | null;

View file

@ -105,7 +105,7 @@ export function createTextField<T extends string>(
export function createBooleanField<T extends string>(
name: T,
label: string,
defaultValue: boolean,
defaultValue: boolean | undefined,
labels: Record<"positive" | "negative", string>
): IFilterElement<T> {
return {
@ -124,6 +124,6 @@ export function createBooleanField<T extends string>(
}
],
type: FieldType.boolean,
value: [defaultValue.toString()]
value: [defaultValue?.toString()]
};
}