diff --git a/CHANGELOG.md b/CHANGELOG.md index 89f6d0f45..bc7c642f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ All notable, unreleased changes to this project will be documented in this file. - Allow sorting products by attribute - #180 by @dominik-zeglen - Hide variants and attributes if product has none - #179 by @dominik-zeglen - Add service account section - #188 by @dominik-zeglen +- Add webhook section - #206 by @benekex2 - Add variant creator - #177 by @dominik-zeglen - Add git hooks - #209 by @dominik-zeglen - Do not send customer invitation email - #211 by @dominik-zeglen diff --git a/schema.graphql b/schema.graphql index 2cac71003..4efcdaaa4 100644 --- a/schema.graphql +++ b/schema.graphql @@ -604,7 +604,7 @@ type Checkout implements Node { privateMeta: [MetaStore]! meta: [MetaStore]! availableShippingMethods: [ShippingMethod]! - availablePaymentGateways: [String]! + availablePaymentGateways: [PaymentGateway]! email: String! isShippingRequired: Boolean! lines: [CheckoutLine] @@ -625,13 +625,13 @@ type CheckoutBillingAddressUpdate { checkoutErrors: [CheckoutError!] } -type CheckoutClearStoredMeta { +type CheckoutClearMeta { errors: [Error!] checkoutErrors: [CheckoutError!] checkout: Checkout } -type CheckoutClearStoredPrivateMeta { +type CheckoutClearPrivateMeta { errors: [Error!] checkoutErrors: [CheckoutError!] checkout: Checkout @@ -1610,6 +1610,11 @@ input FulfillmentUpdateTrackingInput { notifyCustomer: Boolean } +type GatewayConfigLine { + field: String! + value: String +} + scalar GenericScalar type Geolocalization { @@ -2107,12 +2112,16 @@ type Mutations { orderAddNote(order: ID!, input: OrderAddNoteInput!): OrderAddNote orderCancel(id: ID!, restock: Boolean!): OrderCancel orderCapture(amount: Decimal!, id: ID!): OrderCapture + orderClearPrivateMeta(id: ID!, input: MetaPath!): OrderClearPrivateMeta + orderClearMeta(id: ID!, input: MetaPath!): OrderClearMeta orderFulfillmentCancel(id: ID!, input: FulfillmentCancelInput!): FulfillmentCancel orderFulfillmentCreate(input: FulfillmentCreateInput!, order: ID): FulfillmentCreate orderFulfillmentUpdateTracking(id: ID!, input: FulfillmentUpdateTrackingInput!): FulfillmentUpdateTracking orderMarkAsPaid(id: ID!): OrderMarkAsPaid orderRefund(amount: Decimal!, id: ID!): OrderRefund orderUpdate(id: ID!, input: OrderUpdateInput!): OrderUpdate + orderUpdateMeta(id: ID!, input: MetaInput!): OrderUpdateMeta + orderUpdatePrivateMeta(id: ID!, input: MetaInput!): OrderUpdatePrivateMeta orderUpdateShipping(order: ID!, input: OrderUpdateShippingInput): OrderUpdateShipping orderVoid(id: ID!): OrderVoid orderBulkCancel(ids: [ID]!, restock: Boolean!): OrderBulkCancel @@ -2165,14 +2174,14 @@ type Mutations { checkoutShippingMethodUpdate(checkoutId: ID, shippingMethodId: ID!): CheckoutShippingMethodUpdate checkoutUpdateVoucher(checkoutId: ID!, voucherCode: String): CheckoutUpdateVoucher checkoutUpdateMetadata(id: ID!, input: MetaInput!): CheckoutUpdateMeta - checkoutClearMetadata(id: ID!, input: MetaPath!): CheckoutClearStoredMeta + checkoutClearMetadata(id: ID!, input: MetaPath!): CheckoutClearMeta checkoutUpdatePrivateMetadata(id: ID!, input: MetaInput!): CheckoutUpdatePrivateMeta - checkoutClearPrivateMetadata(id: ID!, input: MetaPath!): CheckoutClearStoredPrivateMeta + checkoutClearPrivateMetadata(id: ID!, input: MetaPath!): CheckoutClearPrivateMeta requestPasswordReset(email: String!, redirectUrl: String!): RequestPasswordReset setPassword(token: String!, email: String!, password: String!): SetPassword passwordChange(newPassword: String!, oldPassword: String!): PasswordChange userUpdateMetadata(id: ID!, input: MetaInput!): UserUpdateMeta - userClearStoredMetadata(id: ID!, input: MetaPath!): UserClearStoredMeta + userClearMetadata(id: ID!, input: MetaPath!): UserClearMeta accountAddressCreate(input: AddressInput!, type: AddressTypeEnum): AccountAddressCreate accountAddressUpdate(id: ID!, input: AddressInput!): AccountAddressUpdate accountAddressDelete(id: ID!): AccountAddressDelete @@ -2202,12 +2211,12 @@ type Mutations { userAvatarDelete: UserAvatarDelete userBulkSetActive(ids: [ID]!, isActive: Boolean!): UserBulkSetActive userUpdatePrivateMetadata(id: ID!, input: MetaInput!): UserUpdatePrivateMeta - userClearStoredPrivateMetadata(id: ID!, input: MetaPath!): UserClearStoredPrivateMeta + userClearPrivateMetadata(id: ID!, input: MetaPath!): UserClearPrivateMeta serviceAccountCreate(input: ServiceAccountInput!): ServiceAccountCreate serviceAccountUpdate(id: ID!, input: ServiceAccountInput!): ServiceAccountUpdate serviceAccountDelete(id: ID!): ServiceAccountDelete serviceAccountUpdatePrivateMetadata(id: ID!, input: MetaInput!): ServiceAccountUpdatePrivateMeta - serviceAccountClearStoredPrivateMetadata(id: ID!, input: MetaPath!): ServiceAccountClearStoredPrivateMeta + serviceAccountClearPrivateMetadata(id: ID!, input: MetaPath!): ServiceAccountClearPrivateMeta serviceAccountTokenCreate(input: ServiceAccountTokenInput!): ServiceAccountTokenCreate serviceAccountTokenDelete(id: ID!): ServiceAccountTokenDelete passwordReset(email: String!): PasswordReset @@ -2252,6 +2261,8 @@ type Order implements Node { displayGrossPrices: Boolean! customerNote: String! weight: Weight + privateMeta: [MetaStore]! + meta: [MetaStore]! fulfillments: [Fulfillment]! lines: [OrderLine]! actions: [OrderAction]! @@ -2310,6 +2321,16 @@ type OrderCapture { orderErrors: [OrderError!] } +type OrderClearMeta { + errors: [Error!] + order: Order +} + +type OrderClearPrivateMeta { + errors: [Error!] + order: Order +} + type OrderCountableConnection { pageInfo: PageInfo! edges: [OrderCountableEdge!]! @@ -2504,6 +2525,16 @@ input OrderUpdateInput { shippingAddress: AddressInput } +type OrderUpdateMeta { + errors: [Error!] + order: Order +} + +type OrderUpdatePrivateMeta { + errors: [Error!] + order: Order +} + type OrderUpdateShipping { errors: [Error!] order: Order @@ -2689,6 +2720,11 @@ enum PaymentErrorCode { UNIQUE } +type PaymentGateway { + name: String! + config: [GatewayConfigLine!]! +} + input PaymentInput { gateway: String! token: String! @@ -2800,11 +2836,11 @@ type Product implements Node { meta: [MetaStore]! url: String! thumbnail(size: Int): Image - availability: ProductPricingInfo @deprecated(reason: "DEPRECATED: Will be removed in Saleor 2.10, Has been renamed to 'pricing'.") + availability: ProductPricingInfo @deprecated(reason: "DEPRECATED: Will be removed in Saleor 2.10, Has been renamed to `pricing`.") pricing: ProductPricingInfo isAvailable: Boolean basePrice: Money - price: Money @deprecated(reason: "DEPRECATED: Will be removed in Saleor 2.10, has been replaced by 'basePrice'") + price: Money @deprecated(reason: "DEPRECATED: Will be removed in Saleor 2.10, has been replaced by `basePrice`") minimalVariantPrice: Money taxType: TaxType attributes: [SelectedAttribute!]! @@ -3160,15 +3196,15 @@ type ProductVariant implements Node { name: String! product: Product! trackInventory: Boolean! - quantity: Int! quantityAllocated: Int! weight: Weight privateMeta: [MetaStore]! meta: [MetaStore]! + quantity: Int! stockQuantity: Int! priceOverride: Money price: Money @deprecated(reason: "DEPRECATED: Will be removed in Saleor 2.10, has been replaced by 'pricing.priceUndiscounted'") - availability: VariantPricingInfo @deprecated(reason: "DEPRECATED: Will be removed in Saleor 2.10, has been renamed to 'pricing'.") + availability: VariantPricingInfo @deprecated(reason: "DEPRECATED: Will be removed in Saleor 2.10, has been renamed to `pricing`.") pricing: VariantPricingInfo isAvailable: Boolean attributes: [SelectedAttribute!]! @@ -3291,7 +3327,7 @@ type ProductVariantUpdatePrivateMeta { type Query { webhook(id: ID!): Webhook - webhooks(before: String, after: String, first: Int, last: Int): WebhookCountableConnection + webhooks(filter: WebhookFilterInput, before: String, after: String, first: Int, last: Int): WebhookCountableConnection translations(kind: TranslatableKinds!, before: String, after: String, first: Int, last: Int): TranslatableItemConnection shop: Shop shippingZone(id: ID!): ShippingZone @@ -3313,7 +3349,7 @@ type Query { reportProductSales(period: ReportingPeriod!, before: String, after: String, first: Int, last: Int): ProductVariantCountableConnection payment(id: ID!): Payment payments(before: String, after: String, first: Int, last: Int): PaymentCountableConnection - paymentClientToken(gateway: String!): String + paymentClientToken(gateway: String!): String @deprecated(reason: "DEPRECATED: Will be removed in Saleor 2.10, use payment gateway config instead in availablePaymentGateways.") page(id: ID, slug: String): Page pages(query: String, filter: PageFilterInput, before: String, after: String, first: Int, last: Int): PageCountableConnection homepageEvents(before: String, after: String, first: Int, last: Int): OrderEventCountableConnection @@ -3484,7 +3520,7 @@ type ServiceAccount implements Node { name: String } -type ServiceAccountClearStoredPrivateMeta { +type ServiceAccountClearPrivateMeta { errors: [Error!] accountErrors: [AccountError!] serviceAccount: ServiceAccount @@ -4026,13 +4062,13 @@ type UserBulkSetActive { accountErrors: [AccountError!] } -type UserClearStoredMeta { +type UserClearMeta { errors: [Error!] accountErrors: [AccountError!] user: User } -type UserClearStoredPrivateMeta { +type UserClearPrivateMeta { errors: [Error!] accountErrors: [AccountError!] user: User @@ -4223,6 +4259,7 @@ type VoucherUpdate { } type Webhook implements Node { + name: String serviceAccount: ServiceAccount! targetUrl: String! isActive: Boolean! @@ -4249,6 +4286,7 @@ type WebhookCreate { } input WebhookCreateInput { + name: String targetUrl: String events: [WebhookEventTypeEnum] serviceAccount: ID @@ -4277,11 +4315,11 @@ enum WebhookErrorCode { } type WebhookEvent { - eventType: String + eventType: WebhookEventTypeEnum } enum WebhookEventTypeEnum { - ALL_EVENTS + ANY_EVENTS ORDER_CREATED ORDER_FULLY_PAID ORDER_UPDATED @@ -4290,6 +4328,11 @@ enum WebhookEventTypeEnum { PRODUCT_CREATED } +input WebhookFilterInput { + search: String + isActive: Boolean +} + type WebhookUpdate { errors: [Error!] webhook: Webhook @@ -4297,6 +4340,7 @@ type WebhookUpdate { } input WebhookUpdateInput { + name: String targetUrl: String events: [WebhookEventTypeEnum] serviceAccount: ID diff --git a/src/config.ts b/src/config.ts index 8789313ed..d6daba2f6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,6 +27,7 @@ export interface AppListViewSettings { [ListViews.SHIPPING_METHODS_LIST]: ListSettings; [ListViews.STAFF_MEMBERS_LIST]: ListSettings; [ListViews.VOUCHER_LIST]: ListSettings; + [ListViews.WEBHOOK_LIST]: ListSettings; } export const defaultListSettings: AppListViewSettings = { [ListViews.CATEGORY_LIST]: { @@ -68,5 +69,8 @@ export const defaultListSettings: AppListViewSettings = { }, [ListViews.VOUCHER_LIST]: { rowNumber: PAGINATE_BY + }, + [ListViews.WEBHOOK_LIST]: { + rowNumber: PAGINATE_BY } }; diff --git a/src/configuration/index.tsx b/src/configuration/index.tsx index 8f70c5297..1ca93d41a 100644 --- a/src/configuration/index.tsx +++ b/src/configuration/index.tsx @@ -15,6 +15,7 @@ import ShippingMethods from "@saleor/icons/ShippingMethods"; import SiteSettings from "@saleor/icons/SiteSettings"; import StaffMembers from "@saleor/icons/StaffMembers"; import Taxes from "@saleor/icons/Taxes"; +import Webhooks from "@saleor/icons/Webhooks"; import { sectionNames } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import { menuListUrl } from "@saleor/navigation/urls"; @@ -27,6 +28,7 @@ import { siteSettingsUrl } from "@saleor/siteSettings/urls"; import { staffListUrl } from "@saleor/staff/urls"; import { taxSection } from "@saleor/taxes/urls"; import { PermissionEnum } from "@saleor/types/globalTypes"; +import { webhooksListUrl } from "@saleor/webhooks/urls"; import ConfigurationPage, { MenuSection } from "./ConfigurationPage"; export function createConfigurationMenu(intl: IntlShape): MenuSection[] { @@ -161,6 +163,15 @@ export function createConfigurationMenu(intl: IntlShape): MenuSection[] { permission: PermissionEnum.MANAGE_SERVICE_ACCOUNTS, title: intl.formatMessage(sectionNames.serviceAccounts), url: serviceListUrl() + }, + { + description: intl.formatMessage({ + defaultMessage: "View and update your webhook and their settings" + }), + icon: , + permission: PermissionEnum.MANAGE_WEBHOOKS, + title: intl.formatMessage(sectionNames.webhooks), + url: webhooksListUrl() } ] } diff --git a/src/containers/SearchServiceAccount/index.tsx b/src/containers/SearchServiceAccount/index.tsx new file mode 100644 index 000000000..dd8cfa60f --- /dev/null +++ b/src/containers/SearchServiceAccount/index.tsx @@ -0,0 +1,34 @@ +import gql from "graphql-tag"; + +import { pageInfoFragment } from "@saleor/queries"; +import TopLevelSearch from "../TopLevelSearch"; +import { + SearchServiceAccount, + SearchServiceAccountVariables +} from "./types/SearchServiceAccount"; + +export const searchServiceAccount = gql` + ${pageInfoFragment} + query SearchServiceAccount($after: String, $first: Int!, $query: String!) { + search: serviceAccounts( + after: $after + first: $first + filter: { search: $query } + ) { + edges { + node { + id + name + } + } + pageInfo { + ...PageInfoFragment + } + } + } +`; + +export default TopLevelSearch< + SearchServiceAccount, + SearchServiceAccountVariables +>(searchServiceAccount); diff --git a/src/containers/SearchServiceAccount/types/SearchServiceAccount.ts b/src/containers/SearchServiceAccount/types/SearchServiceAccount.ts new file mode 100644 index 000000000..33c63466c --- /dev/null +++ b/src/containers/SearchServiceAccount/types/SearchServiceAccount.ts @@ -0,0 +1,42 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: SearchServiceAccount +// ==================================================== + +export interface SearchServiceAccount_search_edges_node { + __typename: "ServiceAccount"; + id: string; + name: string | null; +} + +export interface SearchServiceAccount_search_edges { + __typename: "ServiceAccountCountableEdge"; + node: SearchServiceAccount_search_edges_node; +} + +export interface SearchServiceAccount_search_pageInfo { + __typename: "PageInfo"; + endCursor: string | null; + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; +} + +export interface SearchServiceAccount_search { + __typename: "ServiceAccountCountableConnection"; + edges: SearchServiceAccount_search_edges[]; + pageInfo: SearchServiceAccount_search_pageInfo; +} + +export interface SearchServiceAccount { + search: SearchServiceAccount_search | null; +} + +export interface SearchServiceAccountVariables { + after?: string | null; + first: number; + query: string; +} diff --git a/src/icons/Webhooks.tsx b/src/icons/Webhooks.tsx new file mode 100644 index 000000000..051893abe --- /dev/null +++ b/src/icons/Webhooks.tsx @@ -0,0 +1,15 @@ +import createSvgIcon from "@material-ui/icons/utils/createSvgIcon"; +import React from "react"; + +export const Webhooks = createSvgIcon( + <> + + +); +Webhooks.displayName = "Webhooks"; +export default Webhooks; diff --git a/src/index.tsx b/src/index.tsx index 1cf61065b..7c193a445 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -49,6 +49,7 @@ import StaffSection from "./staff"; import TaxesSection from "./taxes"; import TranslationsSection from "./translations"; import { PermissionEnum } from "./types/globalTypes"; +import WebhooksSection from "./webhooks"; interface ResponseError extends ErrorResponse { networkError?: Error & { @@ -219,6 +220,11 @@ const Routes: React.FC = () => { path="/translations" component={TranslationsSection} /> + = Pick< @@ -181,158 +180,6 @@ export const transformAddressToForm = (data: AddressType) => ({ streetAddress2: maybe(() => data.streetAddress2, "") }); -const taxRatesMessages = defineMessages({ - accommodation: { - defaultMessage: "Accommodation", - description: "tax rate" - }, - admissionToCulturalEvents: { - defaultMessage: "Admission to cultural events", - description: "tax rate" - }, - admissionToEntertainmentEvents: { - defaultMessage: "Admission to entertainment events", - description: "tax rate" - }, - admissionToSportingEvents: { - defaultMessage: "Admission to sporting events", - description: "tax rate" - }, - advertising: { - defaultMessage: "Advertising", - description: "tax rate" - }, - agriculturalSupplies: { - defaultMessage: "Agricultural supplies", - description: "tax rate" - }, - babyFoodstuffs: { - defaultMessage: "Baby foodstuffs", - description: "tax rate" - }, - bikes: { - defaultMessage: "Bikes", - description: "tax rate" - }, - books: { - defaultMessage: "Books", - description: "tax rate" - }, - childrensClothing: { - defaultMessage: "Children's clothing", - description: "tax rate" - }, - domesticFuel: { - defaultMessage: "Domestic fuel", - description: "tax rate" - }, - domesticServices: { - defaultMessage: "Domestic services", - description: "tax rate" - }, - ebooks: { - defaultMessage: "E-books", - description: "tax rate" - }, - foodstuffs: { - defaultMessage: "Foodstuffs", - description: "tax rate" - }, - hotels: { - defaultMessage: "Hotels", - description: "tax rate" - }, - medical: { - defaultMessage: "Medical", - description: "tax rate" - }, - newspapers: { - defaultMessage: "Newspapers", - description: "tax rate" - }, - passengerTransport: { - defaultMessage: "Passenger transport", - description: "tax rate" - }, - pharmaceuticals: { - defaultMessage: "Pharmaceuticals", - description: "tax rate" - }, - propertyRenovations: { - defaultMessage: "Property renovations", - description: "tax rate" - }, - restaurants: { - defaultMessage: "Restaurants", - description: "tax rate" - }, - socialHousing: { - defaultMessage: "Social housing", - description: "tax rate" - }, - standard: { - defaultMessage: "Standard", - description: "tax rate" - }, - water: { - defaultMessage: "Water", - description: "tax rate" - } -}); - -export const translatedTaxRates = (intl: IntlShape) => ({ - [TaxRateType.ACCOMMODATION]: intl.formatMessage( - taxRatesMessages.accommodation - ), - [TaxRateType.ADMISSION_TO_CULTURAL_EVENTS]: intl.formatMessage( - taxRatesMessages.admissionToCulturalEvents - ), - [TaxRateType.ADMISSION_TO_ENTERTAINMENT_EVENTS]: intl.formatMessage( - taxRatesMessages.admissionToEntertainmentEvents - ), - [TaxRateType.ADMISSION_TO_SPORTING_EVENTS]: intl.formatMessage( - taxRatesMessages.admissionToSportingEvents - ), - [TaxRateType.ADVERTISING]: intl.formatMessage(taxRatesMessages.advertising), - [TaxRateType.AGRICULTURAL_SUPPLIES]: intl.formatMessage( - taxRatesMessages.agriculturalSupplies - ), - [TaxRateType.BABY_FOODSTUFFS]: intl.formatMessage( - taxRatesMessages.babyFoodstuffs - ), - [TaxRateType.BIKES]: intl.formatMessage(taxRatesMessages.bikes), - [TaxRateType.BOOKS]: intl.formatMessage(taxRatesMessages.books), - [TaxRateType.CHILDRENS_CLOTHING]: intl.formatMessage( - taxRatesMessages.childrensClothing - ), - [TaxRateType.DOMESTIC_FUEL]: intl.formatMessage( - taxRatesMessages.domesticFuel - ), - [TaxRateType.DOMESTIC_SERVICES]: intl.formatMessage( - taxRatesMessages.domesticServices - ), - [TaxRateType.E_BOOKS]: intl.formatMessage(taxRatesMessages.ebooks), - [TaxRateType.FOODSTUFFS]: intl.formatMessage(taxRatesMessages.foodstuffs), - [TaxRateType.HOTELS]: intl.formatMessage(taxRatesMessages.hotels), - [TaxRateType.MEDICAL]: intl.formatMessage(taxRatesMessages.medical), - [TaxRateType.NEWSPAPERS]: intl.formatMessage(taxRatesMessages.newspapers), - [TaxRateType.PASSENGER_TRANSPORT]: intl.formatMessage( - taxRatesMessages.passengerTransport - ), - [TaxRateType.PHARMACEUTICALS]: intl.formatMessage( - taxRatesMessages.pharmaceuticals - ), - [TaxRateType.PROPERTY_RENOVATIONS]: intl.formatMessage( - taxRatesMessages.propertyRenovations - ), - [TaxRateType.RESTAURANTS]: intl.formatMessage(taxRatesMessages.restaurants), - [TaxRateType.SOCIAL_HOUSING]: intl.formatMessage( - taxRatesMessages.socialHousing - ), - [TaxRateType.STANDARD]: intl.formatMessage(taxRatesMessages.standard), - [TaxRateType.WATER]: intl.formatMessage(taxRatesMessages.water) -}); - export const authorizationKeyTypes = { [AuthorizationKeyType.FACEBOOK]: "Facebook", [AuthorizationKeyType.GOOGLE_OAUTH2]: "Google OAuth2" diff --git a/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx b/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx index 91ae73a5f..03c7ed34b 100644 --- a/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx +++ b/src/orders/components/OrderDraftFinalizeDialog/OrderDraftFinalizeDialog.tsx @@ -5,12 +5,13 @@ import { FormattedMessage, IntlShape, useIntl } from "react-intl"; import ActionDialog from "@saleor/components/ActionDialog"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; -export type OrderDraftFinalizeWarning = - | "no-shipping" - | "no-billing" - | "no-user" - | "no-shipping-method" - | "unnecessary-shipping-method"; +export enum OrderDraftFinalizeWarning { + NO_SHIPPING, + NO_BILLING, + NO_USER, + NO_SHIPPING_METHOD, + UNNECESSARY_SHIPPING_METHOD +} export interface OrderDraftFinalizeDialogProps { confirmButtonState: ConfirmButtonTransitionState; @@ -21,30 +22,29 @@ export interface OrderDraftFinalizeDialogProps { onConfirm: () => void; } -const warningToText = (warning: OrderDraftFinalizeWarning, intl: IntlShape) => { - switch (warning) { - case "no-shipping": - return intl.formatMessage({ - defaultMessage: "No shipping address" - }); - case "no-billing": - return intl.formatMessage({ - defaultMessage: "No billing address" - }); - case "no-user": - return intl.formatMessage({ - defaultMessage: "No user information" - }); - case "no-shipping-method": - return intl.formatMessage({ - defaultMessage: "Some products require shipping, but no method provided" - }); - case "unnecessary-shipping-method": - return intl.formatMessage({ +function translateWarnings( + intl: IntlShape +): Record { + return { + [OrderDraftFinalizeWarning.NO_BILLING]: intl.formatMessage({ + defaultMessage: "No billing address" + }), + [OrderDraftFinalizeWarning.NO_SHIPPING]: intl.formatMessage({ + defaultMessage: "No shipping address" + }), + [OrderDraftFinalizeWarning.NO_SHIPPING_METHOD]: intl.formatMessage({ + defaultMessage: "Some products require shipping, but no method provided" + }), + [OrderDraftFinalizeWarning.NO_USER]: intl.formatMessage({ + defaultMessage: "No user information" + }), + [OrderDraftFinalizeWarning.UNNECESSARY_SHIPPING_METHOD]: intl.formatMessage( + { defaultMessage: "Shipping method provided, but no product requires it" - }); - } -}; + } + ) + }; +} const OrderDraftFinalizeDialog: React.StatelessComponent< OrderDraftFinalizeDialogProps @@ -57,6 +57,7 @@ const OrderDraftFinalizeDialog: React.StatelessComponent< orderNumber }) => { const intl = useIntl(); + const translatedWarnings = translateWarnings(intl); return (
    {warnings.map(warning => ( -
  • {warningToText(warning, intl)}
  • +
  • {translatedWarnings[warning]}
  • ))}
diff --git a/src/orders/views/OrderDetails/index.tsx b/src/orders/views/OrderDetails/index.tsx index 36ee9afd5..4ff24ae2b 100644 --- a/src/orders/views/OrderDetails/index.tsx +++ b/src/orders/views/OrderDetails/index.tsx @@ -38,13 +38,13 @@ import { OrderDetailsMessages } from "./OrderDetailsMessages"; const orderDraftFinalizeWarnings = (order: OrderDetails_order) => { const warnings = [] as OrderDraftFinalizeWarning[]; if (!(order && order.shippingAddress)) { - warnings.push("no-shipping"); + warnings.push(OrderDraftFinalizeWarning.NO_SHIPPING); } if (!(order && order.billingAddress)) { - warnings.push("no-billing"); + warnings.push(OrderDraftFinalizeWarning.NO_BILLING); } if (!(order && (order.user || order.userEmail))) { - warnings.push("no-user"); + warnings.push(OrderDraftFinalizeWarning.NO_USER); } if ( order && @@ -52,7 +52,7 @@ const orderDraftFinalizeWarnings = (order: OrderDetails_order) => { order.lines.filter(line => line.isShippingRequired).length > 0 && order.shippingMethod === null ) { - warnings.push("no-shipping-method"); + warnings.push(OrderDraftFinalizeWarning.NO_SHIPPING_METHOD); } if ( order && @@ -60,7 +60,7 @@ const orderDraftFinalizeWarnings = (order: OrderDetails_order) => { order.lines.filter(line => line.isShippingRequired).length === 0 && order.shippingMethod !== null ) { - warnings.push("unnecessary-shipping-method"); + warnings.push(OrderDraftFinalizeWarning.UNNECESSARY_SHIPPING_METHOD); } return warnings; }; diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap index 7f8731f37..9eeaa4605 100644 --- a/src/storybook/__snapshots__/Stories.test.ts.snap +++ b/src/storybook/__snapshots__/Stories.test.ts.snap @@ -29267,6 +29267,44 @@ exports[`Storyshots Views / Configuration default 1`] = ` +
+
+
+ +
+
+

+ Webhooks +

+

+ View and update your webhook and their settings +

+
+
+
@@ -29745,6 +29783,44 @@ exports[`Storyshots Views / Configuration partial access 1`] = ` +
+
+
+ +
+
+

+ Webhooks +

+

+ View and update your webhook and their settings +

+
+
+
@@ -122638,3 +122714,4215 @@ exports[`Storyshots Views / Translations / Language list no data 1`] = ` `; + +exports[`Storyshots Views / Webhooks / Create webhook default 1`] = ` +
+
+
+
+
+ Create Webhook +
+
+
+
+
+
+
+
+
+ + Webhook Information + +
+
+
+
+
+

+ General Informations +

+
+ +
+ + +
+
+
+
+
+

+ Webhook specific information +

+
+
+ + +
+
+
+
+ +
+ + +
+

+ This URL will receive webhook POST requests +

+
+
+
+ +
+ + +
+

+ secret key is used to create a hash signature with each payload. *optional field +

+
+
+
+
+
+
+
+ + Events + +
+
+
+
+
+

+ Expand or restrict webhooks permissions to register certain events in Saleor system. +

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + Webhook Status + +
+
+
+
+
+

+ If you want to disable this webhook please uncheck the box below. +

+ +
+
+
+
+
+ +
+`; + +exports[`Storyshots Views / Webhooks / Create webhook form errors 1`] = ` +
+
+
+
+
+ Create Webhook +
+
+
+
+
+
+
+
+
+ + Webhook Information + +
+
+
+
+
+

+ General Informations +

+
+ +
+ + +
+
+
+
+
+

+ Webhook specific information +

+
+
+ + +

+ This field is required +

+
+
+
+
+ +
+ + +
+

+ This URL will receive webhook POST requests +

+
+
+
+ +
+ + +
+

+ secret key is used to create a hash signature with each payload. *optional field +

+
+
+
+
+
+
+
+ + Events + +
+
+
+
+
+

+ Expand or restrict webhooks permissions to register certain events in Saleor system. +

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + Webhook Status + +
+
+
+
+
+

+ If you want to disable this webhook please uncheck the box below. +

+ +
+
+
+
+
+ +
+`; + +exports[`Storyshots Views / Webhooks / Create webhook loading 1`] = ` +
+
+
+
+
+ Create Webhook +
+
+
+
+
+
+
+
+
+ + Webhook Information + +
+
+
+
+
+

+ General Informations +

+
+ +
+ + +
+
+
+
+
+

+ Webhook specific information +

+
+
+ + +
+
+
+
+ +
+ + +
+

+ This URL will receive webhook POST requests +

+
+
+
+ +
+ + +
+

+ secret key is used to create a hash signature with each payload. *optional field +

+
+
+
+
+
+
+
+ + Events + +
+
+
+
+
+

+ Expand or restrict webhooks permissions to register certain events in Saleor system. +

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + Webhook Status + +
+
+
+
+
+

+ If you want to disable this webhook please uncheck the box below. +

+ +
+
+
+
+
+ +
+`; + +exports[`Storyshots Views / Webhooks / Delete webhook default 1`] = ` +
+`; + +exports[`Storyshots Views / Webhooks / Webhook details default 1`] = ` +
+
+
+
+
+ ... Details +
+
+
+
+
+
+
+
+
+ + Webhook Information + +
+
+
+
+
+

+ General Informations +

+
+ +
+ + +
+
+
+
+
+

+ Webhook specific information +

+
+
+ + +
+
+
+
+ +
+ + +
+

+ This URL will receive webhook POST requests +

+
+
+
+ +
+ + +
+

+ secret key is used to create a hash signature with each payload. *optional field +

+
+
+
+
+
+
+
+ + Events + +
+
+
+
+
+

+ Expand or restrict webhooks permissions to register certain events in Saleor system. +

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + Webhook Status + +
+
+
+
+
+

+ If you want to disable this webhook please uncheck the box below. +

+ +
+
+
+
+
+ +
+`; + +exports[`Storyshots Views / Webhooks / Webhook details form errors 1`] = ` +
+
+
+
+
+ ... Details +
+
+
+
+
+
+
+
+
+ + Webhook Information + +
+
+
+
+
+

+ General Informations +

+
+ +
+ + +
+
+
+
+
+

+ Webhook specific information +

+
+
+ + +

+ This field is required +

+
+
+
+
+ +
+ + +
+

+ This URL will receive webhook POST requests +

+
+
+
+ +
+ + +
+

+ secret key is used to create a hash signature with each payload. *optional field +

+
+
+
+
+
+
+
+ + Events + +
+
+
+
+
+

+ Expand or restrict webhooks permissions to register certain events in Saleor system. +

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + Webhook Status + +
+
+
+
+
+

+ If you want to disable this webhook please uncheck the box below. +

+ +
+
+
+
+
+ +
+`; + +exports[`Storyshots Views / Webhooks / Webhook details loading 1`] = ` +
+
+
+
+
+ ... Details +
+
+
+
+
+
+
+
+
+ + Webhook Information + +
+
+
+
+
+

+ General Informations +

+
+ +
+ + +
+
+
+
+
+

+ Webhook specific information +

+
+
+ + +
+
+
+
+ +
+ + +
+

+ This URL will receive webhook POST requests +

+
+
+
+ +
+ + +
+

+ secret key is used to create a hash signature with each payload. *optional field +

+
+
+
+
+
+
+
+ + Events + +
+
+
+
+
+

+ Expand or restrict webhooks permissions to register certain events in Saleor system. +

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + Webhook Status + +
+
+
+
+
+

+ If you want to disable this webhook please uncheck the box below. +

+ +
+
+
+
+
+ +
+`; + +exports[`Storyshots Views / Webhooks / Webhook list default 1`] = ` +
+
+
+
+ Webhooks +
+
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ Name + + Service Account + + Action +
+ Webhook Test + + Test Account + + + +
+ Webhook Test 2 + + Test Account 2 + + + +
+
+
+
+
+`; + +exports[`Storyshots Views / Webhooks / Webhook list loading 1`] = ` +
+
+
+
+ Webhooks +
+
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+ Name + + Service Account + + Action +
+ + ‌ + + + + ‌ + + + + +
+
+
+
+
+`; + +exports[`Storyshots Views / Webhooks / Webhook list no data 1`] = ` +
+
+
+
+ Webhooks +
+
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ Name + + Service Account + + Action +
+ No webhooks found +
+
+
+
+
+`; diff --git a/src/storybook/stories/orders/OrderDraftFinalizeDialog.tsx b/src/storybook/stories/orders/OrderDraftFinalizeDialog.tsx index 5df866e84..13195baff 100644 --- a/src/storybook/stories/orders/OrderDraftFinalizeDialog.tsx +++ b/src/storybook/stories/orders/OrderDraftFinalizeDialog.tsx @@ -2,7 +2,8 @@ import { storiesOf } from "@storybook/react"; import React from "react"; import OrderDraftFinalize, { - OrderDraftFinalizeDialogProps + OrderDraftFinalizeDialogProps, + OrderDraftFinalizeWarning } from "../../../orders/components/OrderDraftFinalizeDialog"; import Decorator from "../../Decorator"; @@ -21,6 +22,11 @@ storiesOf("Orders / OrderDraftFinalizeDialog", module) .add("with warnings", () => ( )); diff --git a/src/taxes/components/CountryTaxesPage/CountryTaxesPage.tsx b/src/taxes/components/CountryTaxesPage/CountryTaxesPage.tsx index b1ddf1f72..ad251f9e3 100644 --- a/src/taxes/components/CountryTaxesPage/CountryTaxesPage.tsx +++ b/src/taxes/components/CountryTaxesPage/CountryTaxesPage.tsx @@ -6,7 +6,7 @@ import TableCell from "@material-ui/core/TableCell"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { FormattedMessage, IntlShape, useIntl } from "react-intl"; import AppHeader from "@saleor/components/AppHeader"; import { Container } from "@saleor/components/Container"; @@ -14,7 +14,8 @@ import Grid from "@saleor/components/Grid"; import PageHeader from "@saleor/components/PageHeader"; import Skeleton from "@saleor/components/Skeleton"; import { sectionNames } from "@saleor/intl"; -import { maybe, renderCollection, translatedTaxRates } from "../../../misc"; +import { TaxRateType } from "@saleor/types/globalTypes"; +import { maybe, renderCollection } from "../../../misc"; import { CountryList_shop_countries_vat_reducedRates } from "../../types/CountryList"; const styles = createStyles({ @@ -23,6 +24,111 @@ const styles = createStyles({ } }); +function translateTaxRates(intl: IntlShape): Record { + return { + [TaxRateType.ACCOMMODATION]: intl.formatMessage({ + defaultMessage: "Accommodation", + description: "tax rate" + }), + [TaxRateType.ADMISSION_TO_CULTURAL_EVENTS]: intl.formatMessage({ + defaultMessage: "Admission to cultural events", + description: "tax rate" + }), + [TaxRateType.ADMISSION_TO_ENTERTAINMENT_EVENTS]: intl.formatMessage({ + defaultMessage: "Admission to entertainment events", + description: "tax rate" + }), + [TaxRateType.ADMISSION_TO_SPORTING_EVENTS]: intl.formatMessage({ + defaultMessage: "Admission to sporting events", + description: "tax rate" + }), + [TaxRateType.ADVERTISING]: intl.formatMessage({ + defaultMessage: "Advertising", + description: "tax rate" + }), + [TaxRateType.AGRICULTURAL_SUPPLIES]: intl.formatMessage({ + defaultMessage: "Agricultural supplies", + description: "tax rate" + }), + [TaxRateType.BABY_FOODSTUFFS]: intl.formatMessage({ + defaultMessage: "Baby foodstuffs", + description: "tax rate" + }), + [TaxRateType.BIKES]: intl.formatMessage({ + defaultMessage: "Bikes", + description: "tax rate" + }), + [TaxRateType.BOOKS]: intl.formatMessage({ + defaultMessage: "Books", + description: "tax rate" + }), + [TaxRateType.CHILDRENS_CLOTHING]: intl.formatMessage({ + defaultMessage: "Children's clothing", + description: "tax rate" + }), + [TaxRateType.DOMESTIC_FUEL]: intl.formatMessage({ + defaultMessage: "Domestic fuel", + description: "tax rate" + }), + [TaxRateType.DOMESTIC_SERVICES]: intl.formatMessage({ + defaultMessage: "Domestic services", + description: "tax rate" + }), + [TaxRateType.E_BOOKS]: intl.formatMessage({ + defaultMessage: "E-books", + description: "tax rate" + }), + [TaxRateType.FOODSTUFFS]: intl.formatMessage({ + defaultMessage: "Foodstuffs", + description: "tax rate" + }), + [TaxRateType.HOTELS]: intl.formatMessage({ + defaultMessage: "Hotels", + description: "tax rate" + }), + [TaxRateType.MEDICAL]: intl.formatMessage({ + defaultMessage: "Medical", + description: "tax rate" + }), + [TaxRateType.NEWSPAPERS]: intl.formatMessage({ + defaultMessage: "Newspapers", + description: "tax rate" + }), + [TaxRateType.PASSENGER_TRANSPORT]: intl.formatMessage({ + defaultMessage: "Passenger transport", + description: "tax rate" + }), + [TaxRateType.PHARMACEUTICALS]: intl.formatMessage({ + defaultMessage: "Pharmaceuticals", + description: "tax rate" + }), + [TaxRateType.PROPERTY_RENOVATIONS]: intl.formatMessage({ + defaultMessage: "Property renovations", + description: "tax rate" + }), + [TaxRateType.RESTAURANTS]: intl.formatMessage({ + defaultMessage: "Restaurants", + description: "tax rate" + }), + [TaxRateType.SOCIAL_HOUSING]: intl.formatMessage({ + defaultMessage: "Social housing", + description: "tax rate" + }), + [TaxRateType.STANDARD]: intl.formatMessage({ + defaultMessage: "Standard", + description: "tax rate" + }), + [TaxRateType.WATER]: intl.formatMessage({ + defaultMessage: "Water", + description: "tax rate" + }), + [TaxRateType.WINE]: intl.formatMessage({ + defaultMessage: "Wine", + description: "tax rate" + }) + }; +} + export interface CountryTaxesPageProps { countryName: string; taxCategories: CountryList_shop_countries_vat_reducedRates[]; @@ -37,8 +143,8 @@ const CountryTaxesPage = withStyles(styles, { name: "CountryTaxesPage" })( onBack }: CountryTaxesPageProps & WithStyles) => { const intl = useIntl(); + const translatedTaxRates = translateTaxRates(intl); - const taxRates = translatedTaxRates(intl); return ( @@ -82,7 +188,7 @@ const CountryTaxesPage = withStyles(styles, { name: "CountryTaxesPage" })( > {maybe( - () => taxRates[taxCategory.rateType], + () => translatedTaxRates[taxCategory.rateType], )} diff --git a/src/types.ts b/src/types.ts index 77ffb5cbd..c466b2fe2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,7 +28,8 @@ export enum ListViews { SALES_LIST = "SALES_LIST", SHIPPING_METHODS_LIST = "SHIPPING_METHODS_LIST", STAFF_MEMBERS_LIST = "STAFF_MEMBERS_LIST", - VOUCHER_LIST = "VOUCHER_LIST" + VOUCHER_LIST = "VOUCHER_LIST", + WEBHOOK_LIST = "WEBHOOK_LIST" } export interface ListProps { diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 0c23293c3..1b4c9abd9 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -286,6 +286,24 @@ export enum VoucherTypeEnum { SPECIFIC_PRODUCT = "SPECIFIC_PRODUCT", } +export enum WebhookErrorCode { + GRAPHQL_ERROR = "GRAPHQL_ERROR", + INVALID = "INVALID", + NOT_FOUND = "NOT_FOUND", + REQUIRED = "REQUIRED", + UNIQUE = "UNIQUE", +} + +export enum WebhookEventTypeEnum { + ANY_EVENTS = "ANY_EVENTS", + CUSTOMER_CREATED = "CUSTOMER_CREATED", + ORDER_CANCELLED = "ORDER_CANCELLED", + ORDER_CREATED = "ORDER_CREATED", + ORDER_FULLY_PAID = "ORDER_FULLY_PAID", + ORDER_UPDATED = "ORDER_UPDATED", + PRODUCT_CREATED = "PRODUCT_CREATED", +} + export enum WeightUnitsEnum { G = "G", KG = "KG", @@ -810,6 +828,29 @@ export interface VoucherInput { usageLimit?: number | null; } +export interface WebhookCreateInput { + name?: string | null; + targetUrl?: string | null; + events?: (WebhookEventTypeEnum | null)[] | null; + serviceAccount?: string | null; + isActive?: boolean | null; + secretKey?: string | null; +} + +export interface WebhookFilterInput { + search?: string | null; + isActive?: boolean | null; +} + +export interface WebhookUpdateInput { + name?: string | null; + targetUrl?: string | null; + events?: (WebhookEventTypeEnum | null)[] | null; + serviceAccount?: string | null; + isActive?: boolean | null; + secretKey?: string | null; +} + //============================================================== // END Enums and Input Objects //============================================================== diff --git a/src/webhooks/components/WebhookCreatePage/WebhookCreatePage.tsx b/src/webhooks/components/WebhookCreatePage/WebhookCreatePage.tsx new file mode 100644 index 000000000..4aaddff03 --- /dev/null +++ b/src/webhooks/components/WebhookCreatePage/WebhookCreatePage.tsx @@ -0,0 +1,134 @@ +import AppHeader from "@saleor/components/AppHeader"; +import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; +import Container from "@saleor/components/Container"; +import Form from "@saleor/components/Form"; +import FormSpacer from "@saleor/components/FormSpacer"; +import Grid from "@saleor/components/Grid"; +import PageHeader from "@saleor/components/PageHeader"; +import SaveButtonBar from "@saleor/components/SaveButtonBar"; +import { SearchServiceAccount_search_edges_node } from "@saleor/containers/SearchServiceAccount/types/SearchServiceAccount"; +import { sectionNames } from "@saleor/intl"; +import { maybe } from "@saleor/misc"; +import { WebhookEventTypeEnum } from "@saleor/types/globalTypes"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; +import WebhookEvents from "@saleor/webhooks/components/WebhookEvents"; +import WebhookInfo from "@saleor/webhooks/components/WebhookInfo"; +import WebhookStatus from "@saleor/webhooks/components/WebhookStatus"; +import { WebhookCreate_webhookCreate_webhookErrors } from "@saleor/webhooks/types/WebhookCreate"; +import React from "react"; +import { useIntl } from "react-intl"; + +export interface FormData { + id: string; + events: WebhookEventTypeEnum[]; + isActive: boolean; + name: string; + secretKey: string | null; + targetUrl: string; + serviceAccount: string; + allEvents: boolean; +} + +export interface WebhookCreatePageProps { + disabled: boolean; + errors: WebhookCreate_webhookCreate_webhookErrors[]; + services?: SearchServiceAccount_search_edges_node[]; + saveButtonBarState: ConfirmButtonTransitionState; + fetchServiceAccounts: (data: string) => void; + onBack: () => void; + onSubmit: (data: FormData) => void; +} + +const WebhookCreatePage: React.FC = ({ + disabled, + errors: apiErrors, + saveButtonBarState, + services, + fetchServiceAccounts, + onBack, + onSubmit +}) => { + const intl = useIntl(); + const initialForm: FormData = { + allEvents: false, + events: [], + id: null, + isActive: false, + name: null, + secretKey: "", + serviceAccount: "", + targetUrl: "" + }; + const [selectedServiceAcccount, setSelectedServiceAcccount] = React.useState( + "" + ); + const servicesChoiceList = maybe( + () => + services.map(node => ({ + label: node.name, + value: node.id + })), + [] + ); + + return ( +
+ {({ data, errors, hasChanged, submit, change }) => { + const handleServiceSelect = createSingleAutocompleteSelectHandler( + change, + setSelectedServiceAcccount, + servicesChoiceList + ); + return ( + + + {intl.formatMessage(sectionNames.webhooks)} + + + +
+ +
+
+ + + +
+
+ +
+ ); + }} +
+ ); +}; +WebhookCreatePage.displayName = "WebhookCreatePage"; +export default WebhookCreatePage; diff --git a/src/webhooks/components/WebhookCreatePage/WebhooksCreatePage.stories.tsx b/src/webhooks/components/WebhookCreatePage/WebhooksCreatePage.stories.tsx new file mode 100644 index 000000000..a927deeef --- /dev/null +++ b/src/webhooks/components/WebhookCreatePage/WebhooksCreatePage.stories.tsx @@ -0,0 +1,35 @@ +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import Decorator from "@saleor/storybook/Decorator"; +import { WebhookErrorCode } from "@saleor/types/globalTypes"; +import WebhookCreatePage, { WebhookCreatePageProps } from "./WebhookCreatePage"; + +const props: WebhookCreatePageProps = { + disabled: false, + errors: [], + fetchServiceAccounts: () => undefined, + onBack: () => undefined, + onSubmit: () => undefined, + saveButtonBarState: "default", + services: [] +}; +storiesOf("Views / Webhooks / Create webhook", module) + .addDecorator(Decorator) + .add("default", () => ) + .add("loading", () => ) + .add("form errors", () => ( + ({ + __typename: "WebhookError", + message: "Generic form error", + ...error + }))} + /> + )); diff --git a/src/webhooks/components/WebhookCreatePage/index.ts b/src/webhooks/components/WebhookCreatePage/index.ts new file mode 100644 index 000000000..a1632a319 --- /dev/null +++ b/src/webhooks/components/WebhookCreatePage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./WebhookCreatePage"; +export * from "./WebhookCreatePage"; diff --git a/src/webhooks/components/WebhookDeleteDialog/WebhookDeleteDialog.stories.tsx b/src/webhooks/components/WebhookDeleteDialog/WebhookDeleteDialog.stories.tsx new file mode 100644 index 000000000..1117de287 --- /dev/null +++ b/src/webhooks/components/WebhookDeleteDialog/WebhookDeleteDialog.stories.tsx @@ -0,0 +1,19 @@ +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import Decorator from "@saleor/storybook/Decorator"; +import WebhookDeleteDialog, { + WebhookDeleteDialogProps +} from "./WebhookDeleteDialog"; + +const props: WebhookDeleteDialogProps = { + confirmButtonState: "default", + name: "Magento Importer", + onClose: () => undefined, + onConfirm: () => undefined, + open: true +}; + +storiesOf("Views / Webhooks / Delete webhook", module) + .addDecorator(Decorator) + .add("default", () => ); diff --git a/src/webhooks/components/WebhookDeleteDialog/WebhookDeleteDialog.tsx b/src/webhooks/components/WebhookDeleteDialog/WebhookDeleteDialog.tsx new file mode 100644 index 000000000..05700fefc --- /dev/null +++ b/src/webhooks/components/WebhookDeleteDialog/WebhookDeleteDialog.tsx @@ -0,0 +1,50 @@ +import DialogContentText from "@material-ui/core/DialogContentText"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import ActionDialog from "@saleor/components/ActionDialog"; +import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; + +export interface WebhookDeleteDialogProps { + confirmButtonState: ConfirmButtonTransitionState; + open: boolean; + name: string; + onClose: () => void; + onConfirm: () => void; +} + +const WebhookDeleteDialog: React.FC = ({ + confirmButtonState, + open, + name, + onClose, + onConfirm +}) => { + const intl = useIntl(); + + return ( + + + {name} + }} + /> + + + ); +}; +WebhookDeleteDialog.displayName = "WebhookDeleteDialog"; +export default WebhookDeleteDialog; diff --git a/src/webhooks/components/WebhookDeleteDialog/index.ts b/src/webhooks/components/WebhookDeleteDialog/index.ts new file mode 100644 index 000000000..4f47146e4 --- /dev/null +++ b/src/webhooks/components/WebhookDeleteDialog/index.ts @@ -0,0 +1,2 @@ +export { default } from "./WebhookDeleteDialog"; +export * from "./WebhookDeleteDialog"; diff --git a/src/webhooks/components/WebhookEvents/WebhookEvents.tsx b/src/webhooks/components/WebhookEvents/WebhookEvents.tsx new file mode 100644 index 000000000..0faa64c41 --- /dev/null +++ b/src/webhooks/components/WebhookEvents/WebhookEvents.tsx @@ -0,0 +1,113 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import Typography from "@material-ui/core/Typography"; +import CardTitle from "@saleor/components/CardTitle"; +import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; +import Hr from "@saleor/components/Hr"; +import { ChangeEvent } from "@saleor/hooks/useForm"; +import { WebhookEventTypeEnum } from "@saleor/types/globalTypes"; +import { toggle } from "@saleor/utils/lists"; +import React from "react"; +import { useIntl } from "react-intl"; + +interface WebhookEventsProps { + data: { + allEvents: boolean; + events: string[]; + }; + disabled: boolean; + onChange: (event: ChangeEvent, cb?: () => void) => void; +} + +const WebhookEvents: React.StatelessComponent = ({ + data, + disabled, + onChange +}) => { + const intl = useIntl(); + const eventsEnum = Object.values(WebhookEventTypeEnum); + + const translatedEvents = { + [WebhookEventTypeEnum.ANY_EVENTS]: intl.formatMessage({ + defaultMessage: "Any events", + description: "event" + }), + [WebhookEventTypeEnum.CUSTOMER_CREATED]: intl.formatMessage({ + defaultMessage: "Customer created", + description: "event" + }), + [WebhookEventTypeEnum.ORDER_CANCELLED]: intl.formatMessage({ + defaultMessage: "Order cancelled", + description: "event" + }), + [WebhookEventTypeEnum.ORDER_CREATED]: intl.formatMessage({ + defaultMessage: "Order created", + description: "event" + }), + [WebhookEventTypeEnum.ORDER_FULLY_PAID]: intl.formatMessage({ + defaultMessage: "Order fully paid", + description: "event" + }), + [WebhookEventTypeEnum.ORDER_UPDATED]: intl.formatMessage({ + defaultMessage: "Order updated", + description: "event" + }), + [WebhookEventTypeEnum.PRODUCT_CREATED]: intl.formatMessage({ + defaultMessage: "Product created", + description: "event" + }) + }; + + const handleEventsChange = (event: ChangeEvent) => + onChange({ + target: { + name: "events", + value: toggle(event.target.name, data.events, (a, b) => a === b) + } + }); + + return ( + + + + + {intl.formatMessage({ + defaultMessage: + "Expand or restrict webhooks permissions to register certain events in Saleor system.", + description: "webhook events" + })} + + + {!data.allEvents && ( + <> +
+ {eventsEnum.slice(1).map(event => ( +
+ +
+ ))} + + )} +
+
+ ); +}; +WebhookEvents.displayName = "WebhookEvents"; +export default WebhookEvents; diff --git a/src/webhooks/components/WebhookEvents/index.ts b/src/webhooks/components/WebhookEvents/index.ts new file mode 100644 index 000000000..57185fdd2 --- /dev/null +++ b/src/webhooks/components/WebhookEvents/index.ts @@ -0,0 +1,2 @@ +export { default } from "./WebhookEvents"; +export * from "./WebhookEvents"; diff --git a/src/webhooks/components/WebhookInfo/WebhookInfo.tsx b/src/webhooks/components/WebhookInfo/WebhookInfo.tsx new file mode 100644 index 000000000..de17ae2db --- /dev/null +++ b/src/webhooks/components/WebhookInfo/WebhookInfo.tsx @@ -0,0 +1,154 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import makeStyles from "@material-ui/styles/makeStyles"; +import React from "react"; +import { useIntl } from "react-intl"; + +import CardTitle from "@saleor/components/CardTitle"; +import FormSpacer from "@saleor/components/FormSpacer"; +import Hr from "@saleor/components/Hr"; +import SingleAutocompleteSelectField, { + SingleAutocompleteChoiceType +} from "@saleor/components/SingleAutocompleteSelectField"; +import { ChangeEvent } from "@saleor/hooks/useForm"; +import { commonMessages } from "@saleor/intl"; +import { FormErrors } from "@saleor/types"; +import { WebhookCreate_webhookCreate_webhookErrors } from "@saleor/webhooks/types/WebhookCreate"; +import { FormData } from "../WebhooksDetailsPage"; + +interface WebhookInfoProps { + apiErrors: WebhookCreate_webhookCreate_webhookErrors[]; + data: FormData; + disabled: boolean; + serviceDisplayValue: string; + services: SingleAutocompleteChoiceType[]; + errors: FormErrors<"name" | "targetUrl" | "secretKey">; + onChange: (event: React.ChangeEvent) => void; + serviceOnChange: (event: ChangeEvent) => void; + fetchServiceAccounts: (data: string) => void; +} + +const useStyles = makeStyles(() => ({ + status: { + paddingTop: 20 + }, + title: { + fontSize: 16, + lineHeight: 1.9, + paddingBottom: 10 + } +})); + +const WebhookInfo: React.StatelessComponent = ({ + apiErrors, + data, + disabled, + services, + serviceDisplayValue, + fetchServiceAccounts, + errors, + onChange, + serviceOnChange +}) => { + const classes = useStyles({}); + const intl = useIntl(); + const serviceAccountsError = + apiErrors.filter(error => error.field === null).length > 0; + + return ( + + + + + {intl.formatMessage(commonMessages.generalInformations)} + + + +
+ + + {intl.formatMessage({ + defaultMessage: "Webhook specific information", + description: "webhook specific information" + })} + + + + + + +
+
+ ); +}; +WebhookInfo.displayName = "WebhookInfo"; +export default WebhookInfo; diff --git a/src/webhooks/components/WebhookInfo/index.ts b/src/webhooks/components/WebhookInfo/index.ts new file mode 100644 index 000000000..a1ade6f25 --- /dev/null +++ b/src/webhooks/components/WebhookInfo/index.ts @@ -0,0 +1,2 @@ +export { default } from "./WebhookInfo"; +export * from "./WebhookInfo"; diff --git a/src/webhooks/components/WebhookStatus/WebhookStatus.tsx b/src/webhooks/components/WebhookStatus/WebhookStatus.tsx new file mode 100644 index 000000000..ffbead75f --- /dev/null +++ b/src/webhooks/components/WebhookStatus/WebhookStatus.tsx @@ -0,0 +1,55 @@ +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import Typography from "@material-ui/core/Typography"; +import CardTitle from "@saleor/components/CardTitle"; +import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; +import { ChangeEvent } from "@saleor/hooks/useForm"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { FormData } from "../WebhooksDetailsPage"; + +interface WebhookStatusProps { + data: boolean; + disabled: boolean; + onChange: (event: ChangeEvent, cb?: () => void) => void; +} + +const WebhookStatus: React.FC = ({ + data, + disabled, + onChange +}) => { + const intl = useIntl(); + return ( + + + + + {intl.formatMessage({ + defaultMessage: + "If you want to disable this webhook please uncheck the box below.", + description: "webhook active" + })} + + + + + ); +}; +WebhookStatus.displayName = "WebhookStatus"; +export default WebhookStatus; diff --git a/src/webhooks/components/WebhookStatus/index.ts b/src/webhooks/components/WebhookStatus/index.ts new file mode 100644 index 000000000..6a85d9974 --- /dev/null +++ b/src/webhooks/components/WebhookStatus/index.ts @@ -0,0 +1,2 @@ +export { default } from "./WebhookStatus"; +export * from "./WebhookStatus"; diff --git a/src/webhooks/components/WebhooksDetailsPage/WebhooksDetailsPage.stories.tsx b/src/webhooks/components/WebhooksDetailsPage/WebhooksDetailsPage.stories.tsx new file mode 100644 index 000000000..5e30a9cc9 --- /dev/null +++ b/src/webhooks/components/WebhooksDetailsPage/WebhooksDetailsPage.stories.tsx @@ -0,0 +1,46 @@ +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import Decorator from "@saleor/storybook/Decorator"; +import { WebhookErrorCode } from "@saleor/types/globalTypes"; +import WebhooksDetailsPage, { + WebhooksDetailsPageProps +} from "./WebhooksDetailsPage"; + +const props: WebhooksDetailsPageProps = { + disabled: false, + errors: [], + fetchServiceAccounts: () => undefined, + onBack: () => undefined, + onDelete: () => undefined, + onSubmit: () => undefined, + saveButtonBarState: "default", + services: [], + webhook: null +}; +storiesOf("Views / Webhooks / Webhook details", module) + .addDecorator(Decorator) + .add("default", () => ) + .add("loading", () => ( + + )) + .add("form errors", () => ( + ({ + __typename: "WebhookError", + message: "Generic form error", + ...error + }))} + /> + )); diff --git a/src/webhooks/components/WebhooksDetailsPage/WebhooksDetailsPage.tsx b/src/webhooks/components/WebhooksDetailsPage/WebhooksDetailsPage.tsx new file mode 100644 index 000000000..ba88c0932 --- /dev/null +++ b/src/webhooks/components/WebhooksDetailsPage/WebhooksDetailsPage.tsx @@ -0,0 +1,151 @@ +import AppHeader from "@saleor/components/AppHeader"; +import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; +import Container from "@saleor/components/Container"; +import Form from "@saleor/components/Form"; +import FormSpacer from "@saleor/components/FormSpacer"; +import Grid from "@saleor/components/Grid"; +import PageHeader from "@saleor/components/PageHeader"; +import SaveButtonBar from "@saleor/components/SaveButtonBar"; +import { SearchServiceAccount_search_edges_node } from "@saleor/containers/SearchServiceAccount/types/SearchServiceAccount"; +import useStateFromProps from "@saleor/hooks/useStateFromProps"; +import { sectionNames } from "@saleor/intl"; +import { maybe } from "@saleor/misc"; +import { WebhookEventTypeEnum } from "@saleor/types/globalTypes"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; +import WebhookEvents from "@saleor/webhooks/components/WebhookEvents"; +import WebhookInfo from "@saleor/webhooks/components/WebhookInfo"; +import WebhookStatus from "@saleor/webhooks/components/WebhookStatus"; +import { WebhookCreate_webhookCreate_webhookErrors } from "@saleor/webhooks/types/WebhookCreate"; +import { WebhookDetails_webhook } from "@saleor/webhooks/types/WebhookDetails"; + +import React from "react"; +import { useIntl } from "react-intl"; + +export interface FormData { + id: string; + events: WebhookEventTypeEnum[]; + isActive: boolean; + name: string; + secretKey: string | null; + targetUrl: string; + serviceAccount: string; + allEvents: boolean; +} + +export interface WebhooksDetailsPageProps { + disabled: boolean; + errors: WebhookCreate_webhookCreate_webhookErrors[]; + webhook: WebhookDetails_webhook; + services?: SearchServiceAccount_search_edges_node[]; + saveButtonBarState: ConfirmButtonTransitionState; + onBack: () => void; + onDelete: () => void; + fetchServiceAccounts: (data: string) => void; + onSubmit: (data: FormData) => void; +} + +const WebhooksDetailsPage: React.FC = ({ + disabled, + errors: apiErrors, + webhook, + saveButtonBarState, + services, + fetchServiceAccounts, + onBack, + onDelete, + onSubmit +}) => { + const intl = useIntl(); + const initialForm: FormData = { + allEvents: !!maybe(() => webhook.events, []).find( + event => event.eventType === WebhookEventTypeEnum.ANY_EVENTS + ), + events: maybe(() => webhook.events, []) + .map(event => event.eventType) + .filter(event => event !== WebhookEventTypeEnum.ANY_EVENTS), + id: maybe(() => webhook.id, null), + isActive: maybe(() => webhook.isActive, false), + name: maybe(() => webhook.name, ""), + secretKey: maybe(() => webhook.secretKey, ""), + serviceAccount: maybe(() => webhook.serviceAccount.id, ""), + targetUrl: maybe(() => webhook.targetUrl, "") + }; + const [ + selectedServiceAcccounts, + setSelectedServiceAcccounts + ] = useStateFromProps(maybe(() => webhook.serviceAccount.name, "")); + const servicesChoiceList = maybe( + () => + services.map(node => ({ + label: node.name, + value: node.id + })), + [] + ); + return ( +
+ {({ data, errors, hasChanged, submit, change }) => { + const handleServiceSelect = createSingleAutocompleteSelectHandler( + change, + setSelectedServiceAcccounts, + servicesChoiceList + ); + return ( + + + {intl.formatMessage(sectionNames.webhooks)} + + webhook.name, "...") + } + )} + /> + +
+ +
+
+ + + +
+
+ +
+ ); + }} +
+ ); +}; +WebhooksDetailsPage.displayName = "WebhooksDetailsPage"; +export default WebhooksDetailsPage; diff --git a/src/webhooks/components/WebhooksDetailsPage/index.ts b/src/webhooks/components/WebhooksDetailsPage/index.ts new file mode 100644 index 000000000..c09cb2fe6 --- /dev/null +++ b/src/webhooks/components/WebhooksDetailsPage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./WebhooksDetailsPage"; +export * from "./WebhooksDetailsPage"; diff --git a/src/webhooks/components/WebhooksList/WebhooksList.tsx b/src/webhooks/components/WebhooksList/WebhooksList.tsx new file mode 100644 index 000000000..823407bbb --- /dev/null +++ b/src/webhooks/components/WebhooksList/WebhooksList.tsx @@ -0,0 +1,166 @@ +import Card from "@material-ui/core/Card"; +import IconButton from "@material-ui/core/IconButton"; +import { Theme } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +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 DeleteIcon from "@material-ui/icons/Delete"; +import EditIcon from "@material-ui/icons/Edit"; +import makeStyles from "@material-ui/styles/makeStyles"; +import React from "react"; +import { useIntl } from "react-intl"; + +import Skeleton from "@saleor/components/Skeleton"; +import TablePagination from "@saleor/components/TablePagination"; +import { maybe, renderCollection, stopPropagation } from "@saleor/misc"; +import { ListProps } from "@saleor/types"; +import { Webhooks_webhooks_edges_node } from "../../types/Webhooks"; + +export interface WebhooksListProps extends ListProps { + webhooks: Webhooks_webhooks_edges_node[]; + onRemove: (id: string) => void; +} + +const useStyles = makeStyles((theme: Theme) => ({ + [theme.breakpoints.up("lg")]: { + colAction: { + "& svg": { + color: theme.palette.primary.main + }, + textAlign: "right" + }, + colActive: {}, + colName: { + "&&": { + width: "auto" + } + } + }, + colAction: {}, + colActive: {}, + colName: { + paddingLeft: 0, + width: 250 + }, + table: { + tableLayout: "fixed" + }, + tableRow: { + cursor: "pointer" + } +})); + +const numberOfColumns = 3; + +const WebhooksList: React.FC = ({ + settings, + webhooks, + disabled, + onNextPage, + pageInfo, + onRowClick, + onRemove, + onUpdateListSettings, + onPreviousPage +}) => { + const intl = useIntl(); + const classes = useStyles({}); + return ( + + + + + + {intl.formatMessage({ + defaultMessage: "Name", + description: "webhook name" + })} + + + {intl.formatMessage({ + defaultMessage: "Service Account", + description: "webhook service account" + })} + + + {intl.formatMessage({ + defaultMessage: "Action", + description: "user action bar" + })} + + + + + + + + + + {renderCollection( + webhooks, + webhook => ( + + + {maybe(() => webhook.name, )} + + + {maybe( + () => webhook.serviceAccount.name, + + )} + + + + + + onRemove(webhook.id)) + : undefined + } + > + + + + + ), + () => ( + + + {intl.formatMessage({ + defaultMessage: "No webhooks found" + })} + + + ) + )} + +
+
+ ); +}; +WebhooksList.displayName = "WebhooksList"; +export default WebhooksList; diff --git a/src/webhooks/components/WebhooksList/index.ts b/src/webhooks/components/WebhooksList/index.ts new file mode 100644 index 000000000..a9be5a3b1 --- /dev/null +++ b/src/webhooks/components/WebhooksList/index.ts @@ -0,0 +1,2 @@ +export { default } from "./WebhooksList"; +export * from "./WebhooksList"; diff --git a/src/webhooks/components/WebhooksListPage/WebhookListPage.stories.tsx b/src/webhooks/components/WebhooksListPage/WebhookListPage.stories.tsx new file mode 100644 index 000000000..f772fdf9f --- /dev/null +++ b/src/webhooks/components/WebhooksListPage/WebhookListPage.stories.tsx @@ -0,0 +1,30 @@ +import { storiesOf } from "@storybook/react"; +import React from "react"; + +import { + listActionsProps, + pageListProps, + searchPageProps, + tabPageProps +} from "@saleor/fixtures"; +import Decorator from "@saleor/storybook/Decorator"; +import { webhookList } from "../../fixtures"; +import WebhooksListPage, { WebhooksListPageProps } from "./WebhooksListPage"; + +const props: WebhooksListPageProps = { + ...listActionsProps, + ...pageListProps.default, + ...searchPageProps, + ...tabPageProps, + onBack: () => undefined, + onRemove: () => undefined, + webhooks: webhookList +}; + +storiesOf("Views / Webhooks / Webhook list", module) + .addDecorator(Decorator) + .add("default", () => ) + .add("loading", () => ( + + )) + .add("no data", () => ); diff --git a/src/webhooks/components/WebhooksListPage/WebhooksListPage.tsx b/src/webhooks/components/WebhooksListPage/WebhooksListPage.tsx new file mode 100644 index 000000000..f184c04fb --- /dev/null +++ b/src/webhooks/components/WebhooksListPage/WebhooksListPage.tsx @@ -0,0 +1,76 @@ +import Button from "@material-ui/core/Button"; +import Card from "@material-ui/core/Card"; +import React from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import AppHeader from "@saleor/components/AppHeader"; +import Container from "@saleor/components/Container"; +import PageHeader from "@saleor/components/PageHeader"; +import SearchBar from "@saleor/components/SearchBar"; +import { sectionNames } from "@saleor/intl"; +import { PageListProps, SearchPageProps, TabPageProps } from "@saleor/types"; +import { Webhooks_webhooks_edges_node } from "../../types/Webhooks"; +import WebhooksList from "../WebhooksList/WebhooksList"; + +export interface WebhooksListPageProps + extends PageListProps, + SearchPageProps, + TabPageProps { + webhooks: Webhooks_webhooks_edges_node[]; + onBack: () => void; + onRemove: (id: string) => void; +} + +const WebhooksListPage: React.StatelessComponent = ({ + currentTab, + initialSearch, + onAdd, + onAll, + onBack, + onSearchChange, + onTabChange, + onTabDelete, + onTabSave, + tabs, + webhooks, + ...listProps +}) => { + const intl = useIntl(); + return ( + + + {intl.formatMessage(sectionNames.configuration)} + + + + + + + + + + ); +}; +WebhooksListPage.displayName = "WebhooksListPage"; +export default WebhooksListPage; diff --git a/src/webhooks/components/WebhooksListPage/index.ts b/src/webhooks/components/WebhooksListPage/index.ts new file mode 100644 index 000000000..68f52d520 --- /dev/null +++ b/src/webhooks/components/WebhooksListPage/index.ts @@ -0,0 +1,2 @@ +export { default } from "./WebhooksListPage"; +export * from "./WebhooksListPage"; diff --git a/src/webhooks/fixtures.ts b/src/webhooks/fixtures.ts new file mode 100644 index 000000000..3d828832d --- /dev/null +++ b/src/webhooks/fixtures.ts @@ -0,0 +1,41 @@ +import { WebhookDetails_webhook } from "./types/WebhookDetails"; +import { Webhooks_webhooks_edges_node } from "./types/Webhooks"; + +export const webhookList: Webhooks_webhooks_edges_node[] = [ + { + __typename: "Webhook", + id: "Jzx123sEt==", + isActive: true, + name: "Webhook Test", + serviceAccount: { + __typename: "ServiceAccount", + id: "Jzx123sEt==", + name: "Test Account" + } + }, + { + __typename: "Webhook", + id: "Jzx123sEt==", + isActive: true, + name: "Webhook Test 2", + serviceAccount: { + __typename: "ServiceAccount", + id: "Jzx1ss23sEt==", + name: "Test Account 2" + } + } +]; +export const webhook: WebhookDetails_webhook = { + __typename: "Webhook", + events: [], + id: "Jzx123sEt==", + isActive: true, + name: "Webhook Test 2", + secretKey: "zxczx_asdas", + serviceAccount: { + __typename: "ServiceAccount", + id: "Jzx1ss23sEt==", + name: "Test Account 2" + }, + targetUrl: "http://www.getsaleor.com" +}; diff --git a/src/webhooks/index.tsx b/src/webhooks/index.tsx new file mode 100644 index 000000000..ac0497816 --- /dev/null +++ b/src/webhooks/index.tsx @@ -0,0 +1,51 @@ +import { parse as parseQs } from "qs"; +import React from "react"; +import { useIntl } from "react-intl"; +import { Route, RouteComponentProps, Switch } from "react-router-dom"; + +import { sectionNames } from "@saleor/intl"; +import { WindowTitle } from "../components/WindowTitle"; +import { + webhooksAddUrl, + webhooksListPath, + WebhooksListUrlQueryParams, + webhooksPath +} from "./urls"; +import WebhookCreate from "./views/WebhooksCreate"; +import WebhooksDetails from "./views/WebhooksDetails"; +import WebhooksList from "./views/WebhooksList"; + +const WebhookList: React.StatelessComponent> = ({ + location +}) => { + const qs = parseQs(location.search.substr(1)); + const params: WebhooksListUrlQueryParams = qs; + return ; +}; + +const WebhookDetails: React.StatelessComponent> = ({ + match +}) => { + const qs = parseQs(location.search.substr(1)); + const params: WebhooksListUrlQueryParams = qs; + + return ( + + ); +}; + +const Component = () => { + const intl = useIntl(); + return ( + <> + + + + + + + + ); +}; + +export default Component; diff --git a/src/webhooks/mutations.ts b/src/webhooks/mutations.ts new file mode 100644 index 000000000..126e853cf --- /dev/null +++ b/src/webhooks/mutations.ts @@ -0,0 +1,70 @@ +import gql from "graphql-tag"; + +import { TypedMutation } from "../mutations"; +import { webhooksDetailsFragment } from "./queries"; +import { WebhookCreate, WebhookCreateVariables } from "./types/WebhookCreate"; +import { WebhookDelete, WebhookDeleteVariables } from "./types/WebhookDelete"; +import { WebhookUpdate, WebhookUpdateVariables } from "./types/WebhookUpdate"; + +const webhookCreate = gql` + ${webhooksDetailsFragment} + mutation WebhookCreate($input: WebhookCreateInput!) { + webhookCreate(input: $input) { + errors { + field + message + } + webhookErrors { + code + message + field + } + webhook { + ...WebhooksDetailsFragment + } + } + } +`; +export const TypedWebhookCreate = TypedMutation< + WebhookCreate, + WebhookCreateVariables +>(webhookCreate); + +const webhookUpdate = gql` + ${webhooksDetailsFragment} + mutation WebhookUpdate($id: ID!, $input: WebhookUpdateInput!) { + webhookUpdate(id: $id, input: $input) { + errors { + field + message + } + webhookErrors { + code + message + field + } + webhook { + ...WebhooksDetailsFragment + } + } + } +`; +export const TypedWebhookUpdate = TypedMutation< + WebhookUpdate, + WebhookUpdateVariables +>(webhookUpdate); + +const webhookDelete = gql` + mutation WebhookDelete($id: ID!) { + webhookDelete(id: $id) { + errors { + field + message + } + } + } +`; +export const TypedWebhookDelete = TypedMutation< + WebhookDelete, + WebhookDeleteVariables +>(webhookDelete); diff --git a/src/webhooks/queries.ts b/src/webhooks/queries.ts new file mode 100644 index 000000000..bdfdedf4d --- /dev/null +++ b/src/webhooks/queries.ts @@ -0,0 +1,75 @@ +import gql from "graphql-tag"; + +import { TypedQuery } from "../queries"; +import { WebhookDetails, WebhookDetailsVariables } from "./types/WebhookDetails"; +import { Webhooks, WebhooksVariables } from "./types/Webhooks"; + +export const webhooksFragment = gql` + fragment WebhookFragment on Webhook { + id + name + isActive + serviceAccount { + id + name + } + } +`; + +export const webhooksDetailsFragment = gql` + ${webhooksFragment} + fragment WebhooksDetailsFragment on Webhook { + ...WebhookFragment + } +`; + +const webhooksList = gql` + ${webhooksFragment} + query Webhooks( + $first: Int + $after: String + $last: Int + $before: String + $filter: WebhookFilterInput + ) { + webhooks( + first: $first + after: $after + before: $before + last: $last + filter: $filter + ) { + edges { + node { + ...WebhookFragment + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } +`; +export const TypedWebhooksListQuery = TypedQuery( + webhooksList +); + +const webhooksDetails = gql` + ${webhooksFragment} + query WebhookDetails($id: ID!) { + webhook(id: $id) { + ...WebhookFragment + events { + eventType + } + secretKey + targetUrl + } + } +`; +export const TypedWebhooksDetailsQuery = TypedQuery( + webhooksDetails +); diff --git a/src/webhooks/types/WebhookCreate.ts b/src/webhooks/types/WebhookCreate.ts new file mode 100644 index 000000000..326547084 --- /dev/null +++ b/src/webhooks/types/WebhookCreate.ts @@ -0,0 +1,51 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { WebhookCreateInput, WebhookErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: WebhookCreate +// ==================================================== + +export interface WebhookCreate_webhookCreate_errors { + __typename: "Error"; + field: string | null; + message: string | null; +} + +export interface WebhookCreate_webhookCreate_webhookErrors { + __typename: "WebhookError"; + code: WebhookErrorCode | null; + message: string | null; + field: string | null; +} + +export interface WebhookCreate_webhookCreate_webhook_serviceAccount { + __typename: "ServiceAccount"; + id: string; + name: string | null; +} + +export interface WebhookCreate_webhookCreate_webhook { + __typename: "Webhook"; + id: string; + name: string | null; + isActive: boolean; + serviceAccount: WebhookCreate_webhookCreate_webhook_serviceAccount; +} + +export interface WebhookCreate_webhookCreate { + __typename: "WebhookCreate"; + errors: WebhookCreate_webhookCreate_errors[] | null; + webhookErrors: WebhookCreate_webhookCreate_webhookErrors[] | null; + webhook: WebhookCreate_webhookCreate_webhook | null; +} + +export interface WebhookCreate { + webhookCreate: WebhookCreate_webhookCreate | null; +} + +export interface WebhookCreateVariables { + input: WebhookCreateInput; +} diff --git a/src/webhooks/types/WebhookDelete.ts b/src/webhooks/types/WebhookDelete.ts new file mode 100644 index 000000000..7dcfe4301 --- /dev/null +++ b/src/webhooks/types/WebhookDelete.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: WebhookDelete +// ==================================================== + +export interface WebhookDelete_webhookDelete_errors { + __typename: "Error"; + field: string | null; + message: string | null; +} + +export interface WebhookDelete_webhookDelete { + __typename: "WebhookDelete"; + errors: WebhookDelete_webhookDelete_errors[] | null; +} + +export interface WebhookDelete { + webhookDelete: WebhookDelete_webhookDelete | null; +} + +export interface WebhookDeleteVariables { + id: string; +} diff --git a/src/webhooks/types/WebhookDetails.ts b/src/webhooks/types/WebhookDetails.ts new file mode 100644 index 000000000..0f68b655e --- /dev/null +++ b/src/webhooks/types/WebhookDetails.ts @@ -0,0 +1,39 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { WebhookEventTypeEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL query operation: WebhookDetails +// ==================================================== + +export interface WebhookDetails_webhook_serviceAccount { + __typename: "ServiceAccount"; + id: string; + name: string | null; +} + +export interface WebhookDetails_webhook_events { + __typename: "WebhookEvent"; + eventType: WebhookEventTypeEnum | null; +} + +export interface WebhookDetails_webhook { + __typename: "Webhook"; + id: string; + name: string | null; + isActive: boolean; + serviceAccount: WebhookDetails_webhook_serviceAccount; + events: (WebhookDetails_webhook_events | null)[] | null; + secretKey: string | null; + targetUrl: string; +} + +export interface WebhookDetails { + webhook: WebhookDetails_webhook | null; +} + +export interface WebhookDetailsVariables { + id: string; +} diff --git a/src/webhooks/types/WebhookFragment.ts b/src/webhooks/types/WebhookFragment.ts new file mode 100644 index 000000000..ad61edf7b --- /dev/null +++ b/src/webhooks/types/WebhookFragment.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL fragment: WebhookFragment +// ==================================================== + +export interface WebhookFragment_serviceAccount { + __typename: "ServiceAccount"; + id: string; + name: string | null; +} + +export interface WebhookFragment { + __typename: "Webhook"; + id: string; + name: string | null; + isActive: boolean; + serviceAccount: WebhookFragment_serviceAccount; +} diff --git a/src/webhooks/types/WebhookUpdate.ts b/src/webhooks/types/WebhookUpdate.ts new file mode 100644 index 000000000..5f8c14667 --- /dev/null +++ b/src/webhooks/types/WebhookUpdate.ts @@ -0,0 +1,52 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { WebhookUpdateInput, WebhookErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: WebhookUpdate +// ==================================================== + +export interface WebhookUpdate_webhookUpdate_errors { + __typename: "Error"; + field: string | null; + message: string | null; +} + +export interface WebhookUpdate_webhookUpdate_webhookErrors { + __typename: "WebhookError"; + code: WebhookErrorCode | null; + message: string | null; + field: string | null; +} + +export interface WebhookUpdate_webhookUpdate_webhook_serviceAccount { + __typename: "ServiceAccount"; + id: string; + name: string | null; +} + +export interface WebhookUpdate_webhookUpdate_webhook { + __typename: "Webhook"; + id: string; + name: string | null; + isActive: boolean; + serviceAccount: WebhookUpdate_webhookUpdate_webhook_serviceAccount; +} + +export interface WebhookUpdate_webhookUpdate { + __typename: "WebhookUpdate"; + errors: WebhookUpdate_webhookUpdate_errors[] | null; + webhookErrors: WebhookUpdate_webhookUpdate_webhookErrors[] | null; + webhook: WebhookUpdate_webhookUpdate_webhook | null; +} + +export interface WebhookUpdate { + webhookUpdate: WebhookUpdate_webhookUpdate | null; +} + +export interface WebhookUpdateVariables { + id: string; + input: WebhookUpdateInput; +} diff --git a/src/webhooks/types/Webhooks.ts b/src/webhooks/types/Webhooks.ts new file mode 100644 index 000000000..d995aa7f5 --- /dev/null +++ b/src/webhooks/types/Webhooks.ts @@ -0,0 +1,54 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { WebhookFilterInput } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL query operation: Webhooks +// ==================================================== + +export interface Webhooks_webhooks_edges_node_serviceAccount { + __typename: "ServiceAccount"; + id: string; + name: string | null; +} + +export interface Webhooks_webhooks_edges_node { + __typename: "Webhook"; + id: string; + name: string | null; + isActive: boolean; + serviceAccount: Webhooks_webhooks_edges_node_serviceAccount; +} + +export interface Webhooks_webhooks_edges { + __typename: "WebhookCountableEdge"; + node: Webhooks_webhooks_edges_node; +} + +export interface Webhooks_webhooks_pageInfo { + __typename: "PageInfo"; + hasPreviousPage: boolean; + hasNextPage: boolean; + startCursor: string | null; + endCursor: string | null; +} + +export interface Webhooks_webhooks { + __typename: "WebhookCountableConnection"; + edges: Webhooks_webhooks_edges[]; + pageInfo: Webhooks_webhooks_pageInfo; +} + +export interface Webhooks { + webhooks: Webhooks_webhooks | null; +} + +export interface WebhooksVariables { + first?: number | null; + after?: string | null; + last?: number | null; + before?: string | null; + filter?: WebhookFilterInput | null; +} diff --git a/src/webhooks/types/WebhooksDetailsFragment.ts b/src/webhooks/types/WebhooksDetailsFragment.ts new file mode 100644 index 000000000..777f7875c --- /dev/null +++ b/src/webhooks/types/WebhooksDetailsFragment.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL fragment: WebhooksDetailsFragment +// ==================================================== + +export interface WebhooksDetailsFragment_serviceAccount { + __typename: "ServiceAccount"; + id: string; + name: string | null; +} + +export interface WebhooksDetailsFragment { + __typename: "Webhook"; + id: string; + name: string | null; + isActive: boolean; + serviceAccount: WebhooksDetailsFragment_serviceAccount; +} diff --git a/src/webhooks/urls.ts b/src/webhooks/urls.ts new file mode 100644 index 000000000..7319aa36c --- /dev/null +++ b/src/webhooks/urls.ts @@ -0,0 +1,36 @@ +import { stringify as stringifyQs } from "qs"; +import urlJoin from "url-join"; + +import { + ActiveTab, + Dialog, + Filters, + Pagination, + SingleAction, + TabActionDialog +} from "../types"; + +export const webhooksSection = "/webhooks/"; + +export const webhooksListPath = webhooksSection; +export enum WebhookListUrlFiltersEnum { + query = "query" +} +export type WebhookListUrlFilters = Filters; +export type WebhookListUrlDialog = "remove" | TabActionDialog; +export type WebhooksListUrlQueryParams = ActiveTab & + WebhookListUrlFilters & + Dialog & + Pagination & + SingleAction; +export const webhooksListUrl = (params?: WebhooksListUrlQueryParams) => + webhooksListPath + "?" + stringifyQs(params); + +export const webhooksPath = (id: string) => urlJoin(webhooksSection, id); +export type WebhookUrlDialog = "remove"; +export type WebhooksUrlQueryParams = Dialog & SingleAction; +export const webhooksUrl = (id: string, params?: WebhooksUrlQueryParams) => + webhooksPath(encodeURIComponent(id)) + "?" + stringifyQs(params); + +export const webhooksAddPath = urlJoin(webhooksSection, "add"); +export const webhooksAddUrl = webhooksAddPath; diff --git a/src/webhooks/views/WebhooksCreate.tsx b/src/webhooks/views/WebhooksCreate.tsx new file mode 100644 index 000000000..c1af995b2 --- /dev/null +++ b/src/webhooks/views/WebhooksCreate.tsx @@ -0,0 +1,103 @@ +import { WindowTitle } from "@saleor/components/WindowTitle"; +import SearchServiceAccount from "@saleor/containers/SearchServiceAccount"; +import useNavigator from "@saleor/hooks/useNavigator"; +import useNotifier from "@saleor/hooks/useNotifier"; +import { commonMessages } from "@saleor/intl"; +import { WebhookEventTypeEnum } from "@saleor/types/globalTypes"; +import { WebhookCreate as WebhookCreateData } from "@saleor/webhooks/types/WebhookCreate"; +import React from "react"; +import { useIntl } from "react-intl"; +import { DEFAULT_INITIAL_SEARCH_DATA } from "../../config"; +import { getMutationState, maybe } from "../../misc"; +import WebhookCreatePage, { FormData } from "../components/WebhookCreatePage"; +import { TypedWebhookCreate } from "../mutations"; +import { + webhooksListUrl, + WebhooksListUrlQueryParams, + webhooksUrl +} from "../urls"; + +export interface WebhooksCreateProps { + id: string; + params: WebhooksListUrlQueryParams; +} + +export const WebhooksCreate: React.StatelessComponent< + WebhooksCreateProps +> = () => { + const navigate = useNavigator(); + const notify = useNotifier(); + const intl = useIntl(); + + const onSubmit = (data: WebhookCreateData) => { + if (data.webhookCreate.webhookErrors.length === 0) { + notify({ + text: intl.formatMessage(commonMessages.savedChanges) + }); + navigate(webhooksUrl(data.webhookCreate.webhook.id)); + } + }; + + const handleBack = () => navigate(webhooksListUrl()); + + return ( + + {({ search: searchServiceAccount, result: searchServiceAccountOpt }) => ( + + {(webhookCreate, webhookCreateOpts) => { + const handleSubmit = (data: FormData) => + webhookCreate({ + variables: { + input: { + events: data.allEvents + ? [WebhookEventTypeEnum.ANY_EVENTS] + : data.events, + isActive: data.isActive, + name: data.name, + secretKey: data.secretKey, + serviceAccount: data.serviceAccount, + targetUrl: data.targetUrl + } + } + }); + + const formTransitionState = getMutationState( + webhookCreateOpts.called, + webhookCreateOpts.loading, + maybe(() => webhookCreateOpts.data.webhookCreate.webhookErrors) + ); + + return ( + <> + + webhookCreateOpts.data.webhookCreate.webhookErrors, + [] + )} + fetchServiceAccounts={searchServiceAccount} + services={maybe(() => + searchServiceAccountOpt.data.search.edges.map( + edge => edge.node + ) + )} + onBack={handleBack} + onSubmit={handleSubmit} + saveButtonBarState={formTransitionState} + /> + + ); + }} + + )} + + ); +}; +WebhooksCreate.displayName = "WebhooksCreate"; +export default WebhooksCreate; diff --git a/src/webhooks/views/WebhooksDetails.tsx b/src/webhooks/views/WebhooksDetails.tsx new file mode 100644 index 000000000..68b231851 --- /dev/null +++ b/src/webhooks/views/WebhooksDetails.tsx @@ -0,0 +1,168 @@ +import { WindowTitle } from "@saleor/components/WindowTitle"; +import SearchServiceAccount from "@saleor/containers/SearchServiceAccount"; +import useNavigator from "@saleor/hooks/useNavigator"; +import useNotifier from "@saleor/hooks/useNotifier"; +import { commonMessages } from "@saleor/intl"; +import { WebhookEventTypeEnum } from "@saleor/types/globalTypes"; +import WebhookDeleteDialog from "@saleor/webhooks/components/WebhookDeleteDialog"; +import { WebhookDelete } from "@saleor/webhooks/types/WebhookDelete"; +import { WebhookUpdate } from "@saleor/webhooks/types/WebhookUpdate"; +import React from "react"; +import { useIntl } from "react-intl"; +import { DEFAULT_INITIAL_SEARCH_DATA } from "../../config"; +import { getMutationState, maybe } from "../../misc"; +import WebhooksDetailsPage from "../components/WebhooksDetailsPage"; +import { TypedWebhookDelete, TypedWebhookUpdate } from "../mutations"; +import { TypedWebhooksDetailsQuery } from "../queries"; +import { + webhooksListUrl, + WebhooksListUrlQueryParams, + webhooksUrl, + WebhookUrlDialog +} from "../urls"; + +export interface WebhooksDetailsProps { + id: string; + params: WebhooksListUrlQueryParams; +} + +export const WebhooksDetails: React.FC = ({ + id, + params +}) => { + const navigate = useNavigator(); + const notify = useNotifier(); + const intl = useIntl(); + + const closeModal = () => + navigate( + webhooksUrl(id, { + ...params, + action: undefined, + id: undefined + }), + true + ); + + const openModal = (action: WebhookUrlDialog, tokenId?: string) => + navigate( + webhooksUrl(id, { + ...params, + action, + id: tokenId + }) + ); + + const onWebhookDelete = (data: WebhookDelete) => { + if (data.webhookDelete.errors.length === 0) { + notify({ + text: intl.formatMessage(commonMessages.savedChanges) + }); + navigate(webhooksListUrl()); + } + }; + + const onWebhookUpdate = (data: WebhookUpdate) => { + if (data.webhookUpdate.webhookErrors.length === 0) { + notify({ + text: intl.formatMessage(commonMessages.savedChanges) + }); + navigate(webhooksUrl(data.webhookUpdate.webhook.id)); + } + }; + + return ( + + {({ search: searchServiceAccount, result: searchServiceAccountOpt }) => ( + + {(webhookUpdate, webhookUpdateOpts) => ( + + {(webhookDelete, webhookDeleteOpts) => ( + + {webhookDetails => { + const formTransitionState = getMutationState( + webhookUpdateOpts.called, + webhookUpdateOpts.loading, + maybe( + () => webhookUpdateOpts.data.webhookUpdate.webhookErrors + ) + ); + + const handleRemoveConfirm = () => + webhookDelete({ + variables: { + id + } + }); + + const formErrors = maybe( + () => webhookUpdateOpts.data.webhookUpdate.webhookErrors, + [] + ); + + const deleteTransitionState = getMutationState( + webhookDeleteOpts.called, + webhookDeleteOpts.loading, + maybe(() => webhookDeleteOpts.data.webhookDelete.errors) + ); + + return ( + <> + webhookDetails.data.webhook.name)} + /> + webhookDetails.data.webhook)} + fetchServiceAccounts={searchServiceAccount} + services={maybe(() => + searchServiceAccountOpt.data.search.edges.map( + edge => edge.node + ) + )} + onBack={() => navigate(webhooksListUrl())} + onDelete={() => openModal("remove")} + onSubmit={data => { + webhookUpdate({ + variables: { + id, + input: { + events: data.allEvents + ? [WebhookEventTypeEnum.ANY_EVENTS] + : data.events, + isActive: data.isActive, + name: data.name, + secretKey: data.secretKey, + serviceAccount: data.serviceAccount, + targetUrl: data.targetUrl + } + } + }); + }} + /> + webhookDetails.data.webhook.name, + "..." + )} + onClose={closeModal} + onConfirm={handleRemoveConfirm} + open={params.action === "remove"} + /> + + ); + }} + + )} + + )} + + )} + + ); +}; +WebhooksDetails.displayName = "WebhooksDetails"; +export default WebhooksDetails; diff --git a/src/webhooks/views/WebhooksList.tsx b/src/webhooks/views/WebhooksList.tsx new file mode 100644 index 000000000..beeeb9b5d --- /dev/null +++ b/src/webhooks/views/WebhooksList.tsx @@ -0,0 +1,220 @@ +import DeleteFilterTabDialog from "@saleor/components/DeleteFilterTabDialog"; +import SaveFilterTabDialog, { + SaveFilterTabDialogFormData +} from "@saleor/components/SaveFilterTabDialog"; +import { configurationMenuUrl } from "@saleor/configuration"; +import useListSettings from "@saleor/hooks/useListSettings"; +import useNavigator from "@saleor/hooks/useNavigator"; +import useNotifier from "@saleor/hooks/useNotifier"; +import usePaginator, { + createPaginationState +} from "@saleor/hooks/usePaginator"; +import { commonMessages } from "@saleor/intl"; +import { getMutationState, maybe } from "@saleor/misc"; +import { ListViews } from "@saleor/types"; +import WebhookDeleteDialog from "@saleor/webhooks/components/WebhookDeleteDialog"; +import { WebhookDelete } from "@saleor/webhooks/types/WebhookDelete"; +import React from "react"; +import { useIntl } from "react-intl"; + +import WebhooksListPage from "../components/WebhooksListPage/WebhooksListPage"; +import { TypedWebhookDelete } from "../mutations"; +import { TypedWebhooksListQuery } from "../queries"; +import { + WebhookListUrlDialog, + WebhookListUrlFilters, + webhooksAddUrl, + webhooksListUrl, + WebhooksListUrlQueryParams, + webhooksUrl +} from "../urls"; +import { + areFiltersApplied, + deleteFilterTab, + getActiveFilters, + getFilterTabs, + getFilterVariables, + saveFilterTab +} from "./filter"; + +interface WebhooksListProps { + params: WebhooksListUrlQueryParams; +} + +export const WebhooksList: React.FC = ({ params }) => { + const navigate = useNavigator(); + const paginate = usePaginator(); + const notify = useNotifier(); + const intl = useIntl(); + const { updateListSettings, settings } = useListSettings( + ListViews.WEBHOOK_LIST + ); + const tabs = getFilterTabs(); + + const currentTab = + params.activeTab === undefined + ? areFiltersApplied(params) + ? tabs.length + 1 + : 0 + : parseInt(params.activeTab, 0); + + const changeFilterField = (filter: WebhookListUrlFilters) => + navigate( + webhooksListUrl({ + ...getActiveFilters(params), + ...filter, + activeTab: undefined + }) + ); + const closeModal = () => + navigate( + webhooksListUrl({ + ...params, + action: undefined, + id: undefined + }), + true + ); + + const openModal = (action: WebhookListUrlDialog, id?: string) => + navigate( + webhooksListUrl({ + ...params, + action, + id + }) + ); + + const handleTabChange = (tab: number) => { + navigate( + webhooksListUrl({ + activeTab: tab.toString(), + ...getFilterTabs()[tab - 1].data + }) + ); + }; + + const handleTabDelete = () => { + deleteFilterTab(currentTab); + navigate(webhooksListUrl()); + }; + + const handleTabSave = (data: SaveFilterTabDialogFormData) => { + saveFilterTab(data.name, getActiveFilters(params)); + handleTabChange(tabs.length + 1); + }; + + const paginationState = createPaginationState(settings.rowNumber, params); + const queryVariables = React.useMemo( + () => ({ + ...paginationState, + filter: getFilterVariables(params) + }), + [params] + ); + + return ( + + {({ data, loading, refetch }) => { + const onWebhookDelete = (data: WebhookDelete) => { + if (data.webhookDelete.errors.length === 0) { + notify({ + text: intl.formatMessage(commonMessages.savedChanges) + }); + navigate(webhooksListUrl()); + refetch(); + } + }; + return ( + + {(webhookDelete, webhookDeleteOpts) => { + const { loadNextPage, loadPreviousPage, pageInfo } = paginate( + maybe(() => data.webhooks.pageInfo), + paginationState, + params + ); + const handleRemove = (id: string) => { + navigate( + webhooksListUrl({ + ...params, + action: "remove", + id + }) + ); + }; + const handleRemoveConfirm = () => { + webhookDelete({ + variables: { + id: params.id + } + }); + }; + + const deleteTransitionState = getMutationState( + webhookDeleteOpts.called, + webhookDeleteOpts.loading, + maybe(() => webhookDeleteOpts.data.webhookDelete.errors) + ); + + return ( + <> + changeFilterField({ query })} + onAll={() => navigate(webhooksListUrl())} + onTabChange={handleTabChange} + onTabDelete={() => openModal("delete-search")} + onTabSave={() => openModal("save-search")} + tabs={tabs.map(tab => tab.name)} + disabled={loading} + settings={settings} + webhooks={maybe(() => + data.webhooks.edges.map(edge => edge.node) + )} + pageInfo={pageInfo} + onAdd={() => navigate(webhooksAddUrl)} + onBack={() => navigate(configurationMenuUrl)} + onNextPage={loadNextPage} + onPreviousPage={loadPreviousPage} + onRemove={handleRemove} + onUpdateListSettings={updateListSettings} + onRowClick={id => () => navigate(webhooksUrl(id))} + /> + + data.webhooks.edges.find( + edge => edge.node.id === params.id + ).node.name, + "..." + )} + onClose={closeModal} + onConfirm={handleRemoveConfirm} + open={params.action === "remove"} + /> + + tabs[currentTab - 1].name, "...")} + /> + + ); + }} + + ); + }} + + ); +}; + +export default WebhooksList; diff --git a/src/webhooks/views/filter.ts b/src/webhooks/views/filter.ts new file mode 100644 index 000000000..1f7365ebc --- /dev/null +++ b/src/webhooks/views/filter.ts @@ -0,0 +1,28 @@ +import { WebhookFilterInput } from "@saleor/types/globalTypes"; +import { createFilterTabUtils, createFilterUtils } from "../../utils/filters"; +import { + WebhookListUrlFilters, + WebhookListUrlFiltersEnum, + WebhooksListUrlQueryParams +} from "../urls"; + +export const WEBHOOK_FILTERS_KEY = "webhookFilters"; + +export function getFilterVariables( + params: WebhookListUrlFilters +): WebhookFilterInput { + return { + search: params.query + }; +} + +export const { + deleteFilterTab, + getFilterTabs, + saveFilterTab +} = createFilterTabUtils(WEBHOOK_FILTERS_KEY); + +export const { areFiltersApplied, getActiveFilters } = createFilterUtils< + WebhooksListUrlQueryParams, + WebhookListUrlFilters +>(WebhookListUrlFiltersEnum);