diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9a5806962..56f936bee 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -21,10 +21,12 @@ greatly reduce the amount of work needed to review your work. --> 1. [ ] Data-test are added for new elements. 1. [ ] Type definitions are up to date. 1. [ ] Changes are mentioned in the changelog. +1. [ ] The changes are tested in different browsers (Chrome, Firefox, Safari). +1. [ ] The changes are tested in light and dark mode. ### Test environment config -API_URI=https://master.staging.saleor.rocks/graphql/ +API_URI=https://master.staging.saleor.cloud/graphql/ diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index b3f52ea67..f098f9a69 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -11,6 +11,12 @@ jobs: API_URI: https://master.staging.saleor.cloud/graphql/ APP_MOUNT_URI: /dashboard/ STATIC_URL: /dashboard/static/ + SENTRY_ORG: saleor + SENTRY_PROJECT: dashboard + SENTRY_URL_PREFIX: "~/dashboard/static" + ENVIRONMENT: master-staging + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} steps: - uses: actions/checkout@v2 - name: Package @@ -41,4 +47,3 @@ jobs: aws s3 sync build/dashboard s3://${{ secrets.AWS_STAGING_DEPLOYMENT_BUCKET }}/saleor-master-staging/static/ aws s3 cp build/dashboard/index.html s3://${{ secrets.AWS_STAGING_DEPLOYMENT_BUCKET }}/saleor-master-staging/ aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_STAGING_CF_DIST_ID }} --paths "/dashboard*" - diff --git a/.github/workflows/test-env-deploy.yml b/.github/workflows/test-env-deploy.yml index 744b55b66..d7603e822 100644 --- a/.github/workflows/test-env-deploy.yml +++ b/.github/workflows/test-env-deploy.yml @@ -59,7 +59,7 @@ jobs: - name: Run build env: # Use custom API_URI or the default one - API_URI: ${{ steps.api_uri.outputs.custom_api_uri || 'https://master.staging.saleor.rocks/graphql/' }} + API_URI: ${{ steps.api_uri.outputs.custom_api_uri || 'https://master.staging.saleor.cloud/graphql/' }} APP_MOUNT_URI: / STATIC_URL: / run: | @@ -88,7 +88,7 @@ jobs: aws s3 sync ./build/storybook s3://${{ secrets.AWS_TEST_DEPLOYMENT_BUCKET }}/${{ steps.set-domain.outputs.domain }}/storybook - name: Invalidate cache - run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_TEST_CF_DIST_ID }} --paths "/${{ env.domain }}/*" + run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_TEST_CF_DIST_ID }} --paths "/${{ steps.set-domain.outputs.domain }}/*" - name: Update deployment status uses: bobheadxi/deployments@v0.4.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index f629addf2..88c8d972c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,16 @@ All notable, unreleased changes to this project will be documented in this file. - Add shipping methods to translation section - #864 by @marekchoinski - New Miscellaneous and Product refunds - #870 by @orzechdev - Add zip code exclusion - #877 by @dominik-zeglen +- Add order reissue - Update quantity column in Inventory part of Product Variant view - #904 by @dominik-zeglen - Add file attributes - #884 by @orzechdev - Add shipping delivery days - #914 by @orzechdev +- Guard against non-staff users logging in - #947 by @jwm0 +- Add reference attributes - #917 by @orzechdev +- Add product reference attributes - #948 by @orzechdev +- Drop descriptionJson and contentJson fields - #950 by @jwm0 +- Add error tracking with Sentry adapter - #956 by @jwm0 +- Add OAuth2 login with OpenID support - #963 by @orzechdev # 2.11.1 diff --git a/README.md b/README.md index 1bdccfccd..4cda35bd8 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ $ npm i ### Configuration -There are two environment variables available for configuration: +There following environment variables are available for configuration: - `API_URI` (required) - URI of a running instance of Saleor GraphQL API. If you are running Saleor locally with the default settings, set `API_URI` to: `http://localhost:8000/graphql/`. @@ -88,4 +88,43 @@ To build the application bundle run: $ npm run build ``` +### Error Tracking + +Saleor Dashboard is using a generic error tracking wrapper function that takes care of the most popular use cases: + +- initializing the tracker +- capturing exceptions and (optionally) displaying the event id +- setting basic user data (this is opt-in and disabled by default) + +By default it ships with a Sentry adapter but any kind of error tracking software can be used by creating a custom adapter (using Sentry and TS types as an example). + +Example: + +```javascript +// src/services/errorTracking/index.ts + +import { CustomAdapter } from "./adapters/"; + +const errorTracker = ErrorTrackerFactory(CustomAdapter(config)); +``` + +##### Usage with Sentry adapter: + +Sentry is used as the default tracker so no changes in code are necessary and the configuration is done via environment variables. + +The following environment variables are available: + +``` +# Required +SENTRY_DSN= + +# Optional +# https://docs.sentry.io/product/cli/configuration/ +SENTRY_AUTH_TOKEN= +SENTRY_ORG= +SENTRY_PROJECT= +SENTRY_URL_PREFIX= +ENVIRONMENT= +``` + #### Crafted with ❤️ by [Mirumee Software](https://mirumee.com) diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index ef246230d..7ea290962 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -3,6 +3,14 @@ "context": "dialog header", "string": "Cancel Order" }, + "amount title": { + "context": "amount title", + "string": "Refunded amount" + }, + "by preposition": { + "context": "by preposition", + "string": "by" + }, "configurationMenuAttributes": { "string": "Determine attributes used to create product types" }, @@ -42,6 +50,42 @@ "configurationPluginsPages": { "string": "View and update your plugins and their settings." }, + "event products list title refunded": { + "context": "refunded products list title", + "string": "Products refunded" + }, + "event products list title replaced": { + "context": "replaced products list title", + "string": "Products replaced" + }, + "event products list title returned": { + "context": "returned products list title", + "string": "Products returned" + }, + "event products title draft reissued": { + "context": "draft created from replace products list title", + "string": "Products replaced" + }, + "event title draft reissued": { + "context": "draft created from replace event title", + "string": "Draft was reissued from order " + }, + "event title marked as paid": { + "context": "order marked as paid event title", + "string": "Order was marked as paid by" + }, + "event title refunded": { + "context": "refunded event title", + "string": "Products were refunded by " + }, + "event title replaced": { + "context": "replaced event title", + "string": "Products were replaced by " + }, + "event title returned": { + "context": "returned event title", + "string": "Products were returned by" + }, "homeActivityCardHeader": { "context": "header", "string": "Activity" @@ -336,6 +380,10 @@ "context": "unassign product from sale, button", "string": "Unassign" }, + "shipment refund title": { + "context": "shipment refund title", + "string": "Shipment was refunded" + }, "shippingZoneDetailsDialogsDeleteShippingMethod": { "context": "delete shipping method", "string": "Are you sure you want to delete {name}?" @@ -733,37 +781,53 @@ "context": "dialog content", "string": "Are you sure you want to delete {attributeName}?" }, - "src_dot_attributes_dot_components_dot_AttributeDetails_dot_1005562666": { - "context": "attribute's editor component", - "string": "Catalog Input type for Store Owner" + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_attributeLabel": { + "context": "attribute's label", + "string": "Default Label" }, - "src_dot_attributes_dot_components_dot_AttributeDetails_dot_1336738461": { - "context": "product attribute type", - "string": "Dropdown" - }, - "src_dot_attributes_dot_components_dot_AttributeDetails_dot_1376373679": { - "context": "file attribute type", - "string": "File" - }, - "src_dot_attributes_dot_components_dot_AttributeDetails_dot_2592224946": { - "context": "check to require attribute to have value", - "string": "Value Required" - }, - "src_dot_attributes_dot_components_dot_AttributeDetails_dot_3334509011": { - "context": "product attribute type", - "string": "Multiple Select" - }, - "src_dot_attributes_dot_components_dot_AttributeDetails_dot_3605174225": { + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_attributeSlug": { "context": "attribute's slug short code label", "string": "Attribute Code" }, - "src_dot_attributes_dot_components_dot_AttributeDetails_dot_4107478955": { + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_attributeSlugHelperText": { "context": "attribute slug input field helper text", "string": "This is used internally. Make sure you don’t use spaces" }, - "src_dot_attributes_dot_components_dot_AttributeDetails_dot_691600601": { - "context": "attribute's label", - "string": "Default Label" + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_dropdown": { + "context": "product attribute type", + "string": "Dropdown" + }, + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_entityType": { + "context": "attribute's editor component entity", + "string": "Entity" + }, + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_file": { + "context": "file attribute type", + "string": "File" + }, + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_inputType": { + "context": "attribute's editor component", + "string": "Catalog Input type for Store Owner" + }, + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_multiselect": { + "context": "product attribute type", + "string": "Multiple Select" + }, + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_page": { + "context": "page attribute entity type", + "string": "Pages" + }, + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_product": { + "context": "product attribute entity type", + "string": "Products" + }, + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_references": { + "context": "references attribute type", + "string": "References" + }, + "src_dot_attributes_dot_components_dot_AttributeDetails_dot_valueRequired": { + "context": "check to require attribute to have value", + "string": "Value Required" }, "src_dot_attributes_dot_components_dot_AttributeListPage_dot_2417065806": { "context": "tab name", @@ -844,43 +908,46 @@ "context": "page title", "string": "Create New Attribute" }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_1318123158": { - "context": "attribute is filterable in storefront", - "string": "Use in Faceted Navigation" - }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_1877630205": { - "context": "attribute properties regarding storefront", - "string": "Storefront Properties" - }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_26409543": { - "context": "attribute properties regarding dashboard", - "string": "Dashboard Properties" - }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_3135366329": { - "context": "attribute visibility in storefront", - "string": "Public" - }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_3590282519": { - "context": "attribute position in storefront filters", - "string": "Position in faceted navigation" - }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_3758203740": { - "string": "If enabled, attribute will be accessible to customers." - }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_4048785456": { - "string": "If enabled this attribute can be used as a column in product table." - }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_673770329": { + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_availableInGrid": { "context": "add attribute as column in product list table", "string": "Add to Column Options" }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_714335445": { + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_availableInGridCaption": { + "context": "caption", + "string": "If enabled this attribute can be used as a column in product table." + }, + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_dashboardPropertiesTitle": { + "context": "attribute properties regarding dashboard", + "string": "Dashboard Properties" + }, + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_filterableInDashboard": { "context": "use attribute in filtering", "string": "Use in Filtering" }, - "src_dot_attributes_dot_components_dot_AttributeProperties_dot_787251583": { + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_filterableInDashboardCaption": { + "context": "caption", "string": "If enabled, you’ll be able to use this attribute to filter products in product list." }, + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_filterableInStorefront": { + "context": "attribute is filterable in storefront", + "string": "Use in Faceted Navigation" + }, + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_storefrontPropertiesTitle": { + "context": "attribute properties regarding storefront", + "string": "Storefront Properties" + }, + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_storefrontSearchPosition": { + "context": "attribute position in storefront filters", + "string": "Position in faceted navigation" + }, + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_visibleInStorefront": { + "context": "attribute visibility in storefront", + "string": "Public" + }, + "src_dot_attributes_dot_components_dot_AttributeProperties_dot_visibleInStorefrontCaption": { + "context": "caption", + "string": "If enabled, attribute will be accessible to customers." + }, "src_dot_attributes_dot_components_dot_AttributeValueDeleteDialog_dot_1326420604": { "context": "delete attribute value", "string": "Are you sure you want to delete \"{name}\" value?" @@ -949,12 +1016,23 @@ "src_dot_auth_dot_components_dot_LoginPage_dot_2237029987": { "string": "Password" }, + "src_dot_auth_dot_components_dot_LoginPage_dot_2981302356": { + "context": "link", + "string": "Use this link to recover it" + }, "src_dot_auth_dot_components_dot_LoginPage_dot_3476994590": { "string": "Sorry, your username and/or password are incorrect. Please try again." }, - "src_dot_auth_dot_components_dot_LoginPage_dot_4028609483": { - "context": "button", - "string": "Reset your password" + "src_dot_auth_dot_components_dot_LoginPage_dot_3762459576": { + "context": "description", + "string": "or login using" + }, + "src_dot_auth_dot_components_dot_LoginPage_dot_534894384": { + "string": "Sorry, login went wrong. Please try again." + }, + "src_dot_auth_dot_components_dot_LoginPage_dot_599516345": { + "context": "description", + "string": "Forgot password? {resetPasswordLink}" }, "src_dot_auth_dot_components_dot_NewPasswordPage_dot_1254879564": { "string": "New Password" @@ -1502,6 +1580,18 @@ "src_dot_components_dot_AssignAttributeDialog_dot_902296540": { "string": "Search Attributes" }, + "src_dot_components_dot_AssignAttributeValueDialog_dot_header": { + "context": "dialog header", + "string": "Assign Attribute Value" + }, + "src_dot_components_dot_AssignAttributeValueDialog_dot_searchLabel": { + "context": "label", + "string": "Search Attribute Value" + }, + "src_dot_components_dot_AssignAttributeValueDialog_dot_searchPlaceholder": { + "context": "placeholder", + "string": "Search by value name, etc..." + }, "src_dot_components_dot_AssignCategoryDialog_dot_3125506097": { "context": "dialog header", "string": "Assign Category" @@ -1543,6 +1633,10 @@ "src_dot_components_dot_AttributeUnassignDialog_dot_2037985699": { "string": "Are you sure you want to unassign {attributeName} from {itemTypeName}?" }, + "src_dot_components_dot_Attributes_dot_3824528779": { + "context": "button label", + "string": "Assign references" + }, "src_dot_components_dot_Attributes_dot_attributesNumber": { "context": "number of attributes", "string": "{number} Attributes" @@ -3032,6 +3126,9 @@ "src_dot_hooks_dot_3382262667": { "string": "Variant {name} has been set as default." }, + "src_dot_insufficientPermissions": { + "string": "Insufficient permissions" + }, "src_dot_lastName": { "string": "Last Name" }, @@ -3130,13 +3227,17 @@ "src_dot_orders_dot_components_dot_OrderCustomer_dot_4282475982": { "string": "Billing Address" }, - "src_dot_orders_dot_components_dot_OrderDetailsPage_dot_1854613983": { - "context": "button", + "src_dot_orders_dot_components_dot_OrderDetailsPage_dot_cancelOrder": { + "context": "cancel button", "string": "Cancel order" }, - "src_dot_orders_dot_components_dot_OrderDetailsPage_dot_3086420445": { + "src_dot_orders_dot_components_dot_OrderDetailsPage_dot_confirmOrder": { "context": "save button", - "string": "confirm order" + "string": "Confirm order" + }, + "src_dot_orders_dot_components_dot_OrderDetailsPage_dot_returnOrder": { + "context": "return button", + "string": "Return / Replace order" }, "src_dot_orders_dot_components_dot_OrderDraftCancelDialog_dot_1961675716": { "context": "dialog header", @@ -3272,6 +3373,37 @@ "context": "product's sku", "string": "SKU" }, + "src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_1119771899": { + "context": "add tracking button", + "string": "Add tracking" + }, + "src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_2211099657": { + "context": "edit tracking button", + "string": "Edit tracking" + }, + "src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_2845258362": { + "context": "refund button", + "string": "Refund" + }, + "src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_3254150098": { + "string": "Tracking Number: {trackingNumber}" + }, + "src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_732594284": { + "context": "button", + "string": "Cancel Fulfillment" + }, + "src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_fulfilled": { + "context": "fulfillment group", + "string": "Fulfilled from: " + }, + "src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_restocked": { + "context": "restocked group", + "string": "Restocked from: " + }, + "src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_tracking": { + "context": "tracking number", + "string": "Tracking Number: {trackingNumber}" + }, "src_dot_orders_dot_components_dot_OrderFulfillmentCancelDialog_dot_1097287358": { "string": "Are you sure you want to cancel fulfillment? Canceling a fulfillment will restock products at a selected warehouse." }, @@ -3309,71 +3441,10 @@ "context": "dialog header", "string": "Add Tracking Code" }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_1119771899": { - "context": "fulfillment group tracking number", - "string": "Add tracking" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_1134347598": { - "context": "product price", - "string": "Price" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_1895667608": { - "context": "product name", - "string": "Product" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_2211099657": { - "context": "fulfillment group tracking number", - "string": "Edit tracking" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_2567258278": { - "context": "refunded fulfillment, section header", - "string": "Refunded ({quantity})" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_2796503714": { - "context": "ordered product quantity", - "string": "Quantity" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_3254150098": { - "string": "Tracking Number: {trackingNumber}" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_3494686506": { - "context": "section header", - "string": "Fulfilled ({quantity})" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_4039425374": { - "context": "cancelled fulfillment, section header", - "string": "Cancelled ({quantity})" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_693960049": { - "context": "ordered product sku", - "string": "SKU" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_732594284": { - "context": "button", - "string": "Cancel Fulfillment" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_77179533": { - "context": "fulfillment group", - "string": "Fulfilled from: {warehouseName}" - }, - "src_dot_orders_dot_components_dot_OrderFulfillment_dot_878013594": { - "context": "order line total price", - "string": "Total" - }, - "src_dot_orders_dot_components_dot_OrderHistory_dot_1154330234": { - "context": "transaction reference", - "string": "Transaction Reference {transactionReference}" - }, "src_dot_orders_dot_components_dot_OrderHistory_dot_1230178536": { "context": "order history message", "string": "Order address was updated" }, - "src_dot_orders_dot_components_dot_OrderHistory_dot_123236698": { - "string": "Shipment was refunded" - }, - "src_dot_orders_dot_components_dot_OrderHistory_dot_1322321687": { - "string": "Refunded amount" - }, "src_dot_orders_dot_components_dot_OrderHistory_dot_1463685940": { "context": "order history message", "string": "Order was marked as paid" @@ -3489,9 +3560,6 @@ "context": "order history message", "string": "Payment failed" }, - "src_dot_orders_dot_components_dot_OrderHistory_dot_492197448": { - "string": "Products refunded" - }, "src_dot_orders_dot_components_dot_OrderHistory_dot_493321552": { "context": "order history message", "string": "Order cancel information was sent to customer" @@ -3512,6 +3580,14 @@ "context": "order history message", "string": "Order was cancelled" }, + "src_dot_orders_dot_components_dot_OrderHistory_dot_description": { + "context": "replacement created order history message description", + "string": "was created for replaced products" + }, + "src_dot_orders_dot_components_dot_OrderHistory_dot_draftNumber": { + "context": "replacement created order history message draft number", + "string": "Draft #{orderNumber} " + }, "src_dot_orders_dot_components_dot_OrderInvoiceEmailSendDialog_dot_1821123638": { "string": "Are you sure you want to send this invoice: {invoiceNumber} to the customer?" }, @@ -3695,71 +3771,29 @@ "src_dot_orders_dot_components_dot_OrderProductAddDialog_dot_353369701": { "string": "No products matching given query" }, - "src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1580639738": { - "context": "order refund amount", - "string": "Proposed refund amount" + "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_1134347598": { + "context": "product price", + "string": "Price" }, - "src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1705174606": { - "context": "order refund amount", - "string": "Max Refund" + "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_1895667608": { + "context": "product name", + "string": "Product" }, - "src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1734445951": { - "context": "order refund amount", - "string": "Refund total amount" + "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_2796503714": { + "context": "ordered product quantity", + "string": "Quantity" }, - "src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2045860028": { - "context": "order refund amount", - "string": "Authorized Amount" + "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_693960049": { + "context": "ordered product sku", + "string": "SKU" }, - "src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2854815744": { - "context": "order refund amount", - "string": "Previously refunded" + "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_878013594": { + "context": "order line total price", + "string": "Total" }, - "src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2907874606": { - "context": "order refund amount", - "string": "Selected products value" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_79173946": { - "context": "order refund amount", - "string": "Shipment cost" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_120912052": { - "context": "order refund amount", - "string": "Refunded items can’t be fulfilled" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_159210811": { - "string": "Amount must be bigger than 0" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_2256869831": { - "context": "section header", - "string": "Refunded Amount" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_2845258362": { - "context": "order refund amount, input button", - "string": "Refund" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_4033685232": { - "string": "Amount cannot be bigger than max refund" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_40513382": { - "context": "order refund amount, input button", - "string": "Refund {currency} {amount}" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_4224226791": { - "context": "label", - "string": "Automatic Amount" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_508357513": { - "context": "label", - "string": "Manual Amount" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_553737700": { - "context": "checkbox", - "string": "Refund shipment costs" - }, - "src_dot_orders_dot_components_dot_OrderRefundAmount_dot_75546233": { - "context": "order refund amount, input label", - "string": "Amount" + "src_dot_orders_dot_components_dot_OrderRefundFulfilledProducts_dot_1097582574": { + "context": "section header returned", + "string": "Fulfillment returned" }, "src_dot_orders_dot_components_dot_OrderRefundFulfilledProducts_dot_1134347598": { "context": "tabel column header", @@ -3804,6 +3838,90 @@ "context": "page header with order number", "string": "Order #{orderNumber}" }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_2256869831": { + "context": "section header", + "string": "Refunded Amount" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_40513382": { + "context": "order refund amount, input button", + "string": "Refund {currency} {amount}" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_4224226791": { + "context": "label", + "string": "Automatic Amount" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_508357513": { + "context": "label", + "string": "Manual Amount" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_553737700": { + "context": "checkbox", + "string": "Refund shipment costs" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_982301568": { + "context": "label", + "string": "No refund" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_amountTooBig": { + "context": "Amount error message", + "string": "Amount cannot be bigger than max refund" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_amountTooSmall": { + "context": "Amount error message", + "string": "Amount must be bigger than 0" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_authorizedAmount": { + "context": "order refund amount", + "string": "Authorized Amount" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_label": { + "context": "order refund amount, input label", + "string": "Amount" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_maxRefund": { + "context": "order refund amount", + "string": "Max Refund" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_previouslyRefunded": { + "context": "order refund amount", + "string": "Previously refunded" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_proposedRefundAmount": { + "context": "order refund amount", + "string": "Proposed refund amount" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_refundButton": { + "context": "order refund amount button", + "string": "Refund" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_refundCannotBeFulfilled": { + "context": "order refund subtitle", + "string": "Refunded items can't be fulfilled" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_refundTotalAmount": { + "context": "order refund amount", + "string": "Refund total amount" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_replacedProductsValue": { + "context": "order refund amount", + "string": "Replaced Products Value" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_returnButton": { + "context": "order return amount button", + "string": "Return & Replace products" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_returnCannotBeFulfilled": { + "context": "order return subtitle", + "string": "Returned items can't be fulfilled" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_selectedProductsValue": { + "context": "order refund amount", + "string": "Selected Products Value" + }, + "src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_shipmentCost": { + "context": "order refund amount", + "string": "Shipment Cost" + }, "src_dot_orders_dot_components_dot_OrderRefundUnfulfilledProducts_dot_1134347598": { "context": "tabel column header", "string": "Price" @@ -3851,6 +3969,82 @@ "context": "refund type", "string": "Refund Products" }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_1134347598": { + "context": "table column header", + "string": "Price" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_1784788864": { + "context": "table column header", + "string": "Return" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_1895667608": { + "context": "table column header", + "string": "Product" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_2049070632": { + "context": "table column header", + "string": "Replace" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_3988345170": { + "context": "button", + "string": "Set maximal quantities" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_cancelled": { + "context": "cancelled fulfillment, section header", + "string": "Cancelled ({quantity})" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_description": { + "context": "product no longer exists error description", + "string": "This product is no longer in database so it can’t be replaced, nor returned" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_fulfilled": { + "context": "section header", + "string": "Fulfilled ({quantity})" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_improperValue": { + "context": "error message", + "string": "Improper value" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_refunded": { + "context": "refunded fulfillment, section header", + "string": "Refunded ({quantity})" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_refundedAndReturned": { + "context": "cancelled fulfillment, section header", + "string": "Refunded and Returned ({quantity})" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_replaced": { + "context": "refunded fulfillment, section header", + "string": "Replaced ({quantity})" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_returned": { + "context": "refunded fulfillment, section header", + "string": "Returned ({quantity})" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_title": { + "context": "product no longer exists error title", + "string": "Product no longer exists" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_titleFulfilled": { + "context": "section header", + "string": "Fulfillment - #{fulfilmentId}" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_titleUnfulfilled": { + "context": "section header", + "string": "Unfulfilled Items" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_unfulfilled": { + "context": "section header", + "string": "Unfulfilled" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_appTitle": { + "context": "page header with order number", + "string": "Order #{orderNumber}" + }, + "src_dot_orders_dot_components_dot_OrderReturnPage_dot_pageTitle": { + "context": "page header", + "string": "Order no. {orderNumber} - Replace/Return" + }, "src_dot_orders_dot_components_dot_OrderSettingsPage_dot_1149215359": { "context": "header", "string": "Order settings" @@ -3874,34 +4068,10 @@ "context": "dialog header", "string": "Edit Shipping Method" }, - "src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_1134347598": { - "context": "product unit price", - "string": "Price" - }, - "src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_1895667608": { - "context": "product name", - "string": "Product" - }, - "src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_2095687440": { + "src_dot_orders_dot_components_dot_OrderUnfulfilledProductsCard_dot_2095687440": { "context": "button", "string": "Fulfill" }, - "src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_2796503714": { - "context": "ordered products", - "string": "Quantity" - }, - "src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_2886647373": { - "context": "section header", - "string": "Unfulfilled ({quantity})" - }, - "src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_693960049": { - "context": "ordered product sku", - "string": "SKU" - }, - "src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_878013594": { - "context": "order line total price", - "string": "Total" - }, "src_dot_orders_dot_views_dot_OrderDetails_dot_1039259580": { "string": "We’re generating the invoice you requested. Please wait a couple of moments" }, @@ -3991,6 +4161,18 @@ "context": "order refunded success message", "string": "Refunded Items" }, + "src_dot_orders_dot_views_dot_OrderReturn_dot_cannotRefundDescription": { + "context": "order return error description when cannot refund", + "string": "We’ve encountered a problem while refunding the products. Product’s were not refunded. Please try again." + }, + "src_dot_orders_dot_views_dot_OrderReturn_dot_cannotRefundTitle": { + "context": "order return error title when cannot refund", + "string": "Couldn't refund products" + }, + "src_dot_orders_dot_views_dot_OrderReturn_dot_successAlert": { + "context": "order returned success message", + "string": "Successfully returned products!" + }, "src_dot_pageTypes": { "context": "page types section name", "string": "Page Types" @@ -4237,6 +4419,10 @@ "context": "payment status", "string": "Partially refunded" }, + "src_dot_partiallyReturned": { + "context": "order status", + "string": "Partially returned" + }, "src_dot_permissionGroups": { "context": "permission groups section name", "string": "Permission Groups" @@ -5272,6 +5458,10 @@ "src_dot_requiredField": { "string": "This field is required" }, + "src_dot_returned": { + "context": "order status", + "string": "Returned" + }, "src_dot_sales": { "context": "sales section name", "string": "Sales" @@ -6251,6 +6441,9 @@ "src_dot_translations_dot_components_dot_TranslationsVouchersPage_dot_2599922713": { "string": "Voucher Name" }, + "src_dot_unauthorizedDashboardAccess": { + "string": "Only staff users can access the dashboard" + }, "src_dot_unconfirmed": { "context": "order status", "string": "Unconfirmed" @@ -6707,6 +6900,10 @@ "context": "table column header", "string": "Quantity" }, + "transaction reference subtitle": { + "context": "transaction reference subtitle", + "string": "Transaction reference" + }, "voucherDetailsUnassignCategory": { "context": "unassign category from voucher, button", "string": "Unassign" diff --git a/package-lock.json b/package-lock.json index 8ff25d06b..c885d67eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30069,9 +30069,9 @@ } }, "@editorjs/editorjs": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.19.0.tgz", - "integrity": "sha512-8PUVaBZx69IrG8dNrE+FZbHSiRTR8ql8L/cmEi1mOdEdTqnOLq5Wv9dgemK00mBWEgNoavMAjtGQpItGknAa8A==", + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.19.1.tgz", + "integrity": "sha512-5lN7r5B2NCE8VJdsS3poX3Qg9rNwzpxZ+6Jjif3hAVZTYpQwg5wXEpAHFNbuavS0T5Ji+0ID31DQFotVI4PosA==", "requires": { "codex-notifier": "^1.1.2", "codex-tooltip": "^1.0.1" @@ -31110,6 +31110,120 @@ "any-observable": "^0.3.0" } }, + "@sentry/browser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.0.0.tgz", + "integrity": "sha512-R4+MHb5FyVZCz3EVnaquvT1mwOM2MWP4gBqjYEADY5m0XWoHiJf0skFkWt8iEKJanzGbhl4PMb9gHuJj6YfVLw==", + "requires": { + "@sentry/core": "6.0.0", + "@sentry/types": "6.0.0", + "@sentry/utils": "6.0.0", + "tslib": "^1.9.3" + } + }, + "@sentry/cli": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.61.0.tgz", + "integrity": "sha512-pHEhqP1bB4sdO7N5ow/IkRBrPbKT9HZRinq4PhTVIvmG+NW4VVuVZ6k4tlbp+JXmzMcUc/iXynVkTL7zJIlTQw==", + "dev": true, + "requires": { + "https-proxy-agent": "^5.0.0", + "mkdirp": "^0.5.5", + "node-fetch": "^2.6.0", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + } + } + }, + "@sentry/core": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.0.0.tgz", + "integrity": "sha512-afAiOachs/WfGWc9LsJBFnJMhqQVENyzfSMnf7sLRvxPAw8n7IrXY0R09MKmG0SlAnTKN2pWoQFzFF+J3NuHBA==", + "requires": { + "@sentry/hub": "6.0.0", + "@sentry/minimal": "6.0.0", + "@sentry/types": "6.0.0", + "@sentry/utils": "6.0.0", + "tslib": "^1.9.3" + } + }, + "@sentry/hub": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.0.0.tgz", + "integrity": "sha512-s8IsW6LvEH7ACnniQcxxb/9uEyjmoQ/TAoryTJN2qyPzzrHTw8NCyMuJvK+8ivUvRViz5AvtuOFf8AJlh9lzeA==", + "requires": { + "@sentry/types": "6.0.0", + "@sentry/utils": "6.0.0", + "tslib": "^1.9.3" + } + }, + "@sentry/minimal": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.0.0.tgz", + "integrity": "sha512-daYdEzTr+ERMwViu6RpWHOfk0oZrSNqdx+7bejTqmFHqO4pt+9ZrMiw3vinL+MWQcKXwD95uXBz6O/ryrVdPtg==", + "requires": { + "@sentry/hub": "6.0.0", + "@sentry/types": "6.0.0", + "tslib": "^1.9.3" + } + }, + "@sentry/react": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.0.0.tgz", + "integrity": "sha512-GYX110NSodd8wGUbnyxemndTijM+U7dI/WjFSPOyJdLB2hzzPjJ9kUqtuobT/JlGzbWE2278WysAuySne6bUGw==", + "requires": { + "@sentry/browser": "6.0.0", + "@sentry/minimal": "6.0.0", + "@sentry/types": "6.0.0", + "@sentry/utils": "6.0.0", + "hoist-non-react-statics": "^3.3.2", + "tslib": "^1.9.3" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + } + } + }, + "@sentry/types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.0.tgz", + "integrity": "sha512-yueRSRGPCahuju/UMdtOt8LIIncbpwLINQd9Q8E4OXtoPpMHR6Oun8sMKCPd+Wq3piI5yRDzKkGCl+sH7mHVrA==" + }, + "@sentry/utils": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.0.tgz", + "integrity": "sha512-dMMWOT69bQ4CF1R33dOnXIOyiHRWsUAON3nFVljV1JNNTDA69YwaF9f5FIT0DKpO4qhgTlElsm8WgHI9prAVEQ==", + "requires": { + "@sentry/types": "6.0.0", + "tslib": "^1.9.3" + } + }, + "@sentry/webpack-plugin": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-1.14.0.tgz", + "integrity": "sha512-1cS99mnHqASYtMlHi2J107p6x3lfC5NmLOgA0iI6avaaFes8RTQMlW8YT2CyrvhtQod1bViPZOlh3NOVC8vnOA==", + "dev": true, + "requires": { + "@sentry/cli": "^1.58.0" + } + }, "@sindresorhus/fnv1a": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-1.2.0.tgz", @@ -36245,9 +36359,9 @@ "integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==" }, "codex-tooltip": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.1.tgz", - "integrity": "sha512-1xLb1NZbxguNtf02xBRhDphq/EXvMMeEbY0ievjQTHqf8UjXsD41evGk9rqcbjpl+JOjNgtwnp1OaU/X/h6fhQ==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.2.tgz", + "integrity": "sha512-oC+Bu5X/zyhbPydgMSLWKoM/+vkJMqaLWu3Dt/jZgXS3MWK23INwC5DMBrVXZSufAFk0i0SUni38k9rLMyZn/w==" }, "collapse-white-space": { "version": "1.0.6", @@ -46708,6 +46822,12 @@ "ipaddr.js": "1.9.0" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", diff --git a/package.json b/package.json index 15975a82b..d902a4611 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "npm": ">=6.11.0" }, "dependencies": { - "@editorjs/editorjs": "^2.19.0", + "@editorjs/editorjs": "^2.19.1", "@editorjs/header": "^2.6.1", "@editorjs/image": "^2.6.0", "@editorjs/list": "^1.6.1", @@ -27,6 +27,7 @@ "@material-ui/styles": "^4.5.2", "@saleor/macaw-ui": "^0.1.1-9", "@types/faker": "^5.1.6", + "@sentry/react": "^6.0.0", "apollo": "^2.21.2", "apollo-cache-inmemory": "^1.6.5", "apollo-client": "^2.6.8", @@ -94,6 +95,7 @@ "@pollyjs/adapter-node-http": "^5.0.0", "@pollyjs/core": "^5.0.0", "@pollyjs/persister-fs": "^5.0.0", + "@sentry/webpack-plugin": "^1.14.0", "@storybook/addon-storyshots": "^5.2.8", "@storybook/react": "^5.1.9", "@testing-library/react-hooks": "^1.1.0", @@ -228,6 +230,7 @@ "test:e2e:dev": "start-server-and-test start http://localhost:9000 cy:open", "test": "jest src/", "transpile-messages": "node scripts/transpile-tx.js", - "lint": "npx eslint \"src/**/*.@(tsx|ts|jsx|js)\" --fix ; npx prettier --check \"src/**/*.@(tsx|ts|jsx|js)\" --write" + "lint": "npx eslint \"src/**/*.@(tsx|ts|jsx|js)\" --fix ; npx prettier --check \"src/**/*.@(tsx|ts|jsx|js)\" --write", + "postbuild": "rimraf ./build/**/*.js.map" } } diff --git a/recordings/User_3768991250/will-be-logged-if-has-valid-token_3465908808/recording.har b/recordings/User_3768991250/will-be-logged-if-has-valid-token_3465908808/recording.har index 8ad594963..e1740a219 100644 --- a/recordings/User_3768991250/will-be-logged-if-has-valid-token_3465908808/recording.har +++ b/recordings/User_3768991250/will-be-logged-if-has-valid-token_3465908808/recording.har @@ -8,11 +8,11 @@ }, "entries": [ { - "_id": "f515e15cbc83df73e5bd41437971c2e6", + "_id": "a3088678db2635ada66ab049f76c9722", "_order": 0, "cache": {}, "request": { - "bodySize": 691, + "bodySize": 702, "cookies": [], "headers": [ { @@ -28,7 +28,7 @@ { "_fromType": "array", "name": "content-length", - "value": "691" + "value": "702" }, { "_fromType": "array", @@ -56,61 +56,53 @@ "postData": { "mimeType": "application/json", "params": [], - "text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5NjAyODUxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.eo8_Ew98HICB4cFQN2U7mCJ8ydGVOvQLGRT4CnkufMc\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" + "text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMDYzMjg0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU5BPT0iLCJpc19zdGFmZiI6dHJ1ZX0.QDp4vlm1tKhk8iFnY2MnREvO-IubI5j8g_Wylb1XJqc\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" }, "queryString": [], "url": "http://localhost:8000/graphql/" }, "response": { - "bodySize": 1619, + "bodySize": 1765, "content": { "mimeType": "application/json", - "size": 1619, - "text": "[{\"data\": {\"tokenVerify\": {\"payload\": {\"iat\": 1596028218, \"exp\": 1596028518, \"token\": \"C3Sk2kLRVuPA\", \"email\": \"admin@example.com\", \"type\": \"access\", \"user_id\": \"VXNlcjoyMQ==\", \"is_staff\": true}, \"user\": {\"id\": \"VXNlcjoyMQ==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"VerifyToken\"}}}]" + "size": 1765, + "text": "[{\"data\": {\"tokenVerify\": {\"payload\": {\"iat\": 1610632544, \"exp\": 1610632844, \"token\": \"ksEVMvgg6Bfi\", \"email\": \"admin@example.com\", \"type\": \"access\", \"user_id\": \"VXNlcjoyNA==\", \"is_staff\": true}, \"user\": {\"id\": \"VXNlcjoyNA==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": true, \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES\", \"name\": \"Manage product types and attributes.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"VerifyToken\"}}}]" }, "cookies": [], "headers": [ { "name": "date", - "value": "Wed, 29 Jul 2020 13:10:18 GMT" + "value": "Thu, 14 Jan 2021 14:10:40 GMT" }, { "name": "server", - "value": "WSGIServer/0.2 CPython/3.8.1" + "value": "WSGIServer/0.2 CPython/3.8.7" }, { "name": "content-type", "value": "application/json" }, - { - "name": "access-control-allow-origin", - "value": "http://localhost:9000" - }, - { - "name": "access-control-allow-methods", - "value": "POST, OPTIONS" - }, - { - "name": "access-control-allow-headers", - "value": "Origin, Content-Type, Accept, Authorization" - }, { "name": "content-length", - "value": "1619" + "value": "1765" }, { "name": "x-content-type-options", "value": "nosniff" + }, + { + "name": "referrer-policy", + "value": "same-origin" } ], - "headersSize": 336, + "headersSize": 194, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2020-07-29T13:10:18.327Z", - "time": 23, + "startedDateTime": "2021-01-14T14:10:40.434Z", + "time": 155, "timings": { "blocked": -1, "connect": -1, @@ -118,7 +110,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 23 + "wait": 155 } } ], diff --git a/recordings/User_3768991250/will-be-logged-in-if-has-valid-credentials_3587751314/recording.har b/recordings/User_3768991250/will-be-logged-in-if-has-valid-credentials_3587751314/recording.har index be977fd9c..0ac2a2d6a 100644 --- a/recordings/User_3768991250/will-be-logged-in-if-has-valid-credentials_3587751314/recording.har +++ b/recordings/User_3768991250/will-be-logged-in-if-has-valid-credentials_3587751314/recording.har @@ -8,11 +8,11 @@ }, "entries": [ { - "_id": "7c460842cac4a92c188d5451dfc533a2", + "_id": "a2b8a02f624e52cd2b73a831f65d9a52", "_order": 0, "cache": {}, "request": { - "bodySize": 587, + "bodySize": 598, "cookies": [], "headers": [ { @@ -28,7 +28,7 @@ { "_fromType": "array", "name": "content-length", - "value": "587" + "value": "598" }, { "_fromType": "array", @@ -56,74 +56,65 @@ "postData": { "mimeType": "application/json", "params": [], - "text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" + "text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" }, "queryString": [], "url": "http://localhost:8000/graphql/" }, "response": { - "bodySize": 1830, + "bodySize": 1976, "content": { "mimeType": "application/json", - "size": 1830, - "text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"rLPNMGNYKXH8VY4UNEWl4nEOFMseocljioigPl36IM2CqbdmOTEpNwvdHBAJ1ZWQ\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5NjAyODUxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.eo8_Ew98HICB4cFQN2U7mCJ8ydGVOvQLGRT4CnkufMc\", \"user\": {\"id\": \"VXNlcjoyMQ==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]" + "size": 1976, + "text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"UIzzJSFalS8pplfM1j5QNIUNiXb0VFH3kbe6kfTddYvLjJ9DhMasCtHJKXoGDfbw\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMDYzMjg0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU5BPT0iLCJpc19zdGFmZiI6dHJ1ZX0.QDp4vlm1tKhk8iFnY2MnREvO-IubI5j8g_Wylb1XJqc\", \"user\": {\"id\": \"VXNlcjoyNA==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": true, \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES\", \"name\": \"Manage product types and attributes.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]" }, "cookies": [ { "httpOnly": true, "name": "refreshToken", "path": "/", - "secure": true, - "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5ODYyMDIxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6InJMUE5NR05ZS1hIOFZZNFVORVdsNG5FT0ZNc2VvY2xqaW9pZ1BsMzZJTTJDcWJkbU9URXBOd3ZkSEJBSjFaV1EifQ.boD8G4pkSnZF-PLl5oOg85Uj-mqTiAzOkua9aAG3Bz4" + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMzIyNDU0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lOQT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6IlVJenpKU0ZhbFM4cHBsZk0xajVRTklVTmlYYjBWRkgza2JlNmtmVGRkWXZMako5RGhNYXNDdEhKS1hvR0RmYncifQ.Br0GWGPPcnysyUxukjBBfXNbwCAm2qlR5OYClwFF3ZQ" } ], "headers": [ { "name": "date", - "value": "Wed, 29 Jul 2020 13:10:18 GMT" + "value": "Thu, 14 Jan 2021 13:55:44 GMT" }, { "name": "server", - "value": "WSGIServer/0.2 CPython/3.8.1" + "value": "WSGIServer/0.2 CPython/3.8.7" }, { "name": "content-type", "value": "application/json" }, - { - "name": "access-control-allow-origin", - "value": "http://localhost:9000" - }, - { - "name": "access-control-allow-methods", - "value": "POST, OPTIONS" - }, - { - "name": "access-control-allow-headers", - "value": "Origin, Content-Type, Accept, Authorization" - }, { "name": "content-length", - "value": "1830" + "value": "1976" }, { "name": "x-content-type-options", "value": "nosniff" }, + { + "name": "referrer-policy", + "value": "same-origin" + }, { "_fromType": "array", "name": "set-cookie", - "value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5ODYyMDIxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6InJMUE5NR05ZS1hIOFZZNFVORVdsNG5FT0ZNc2VvY2xqaW9pZ1BsMzZJTTJDcWJkbU9URXBOd3ZkSEJBSjFaV1EifQ.boD8G4pkSnZF-PLl5oOg85Uj-mqTiAzOkua9aAG3Bz4; HttpOnly; Path=/; Secure" + "value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMzIyNDU0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lOQT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6IlVJenpKU0ZhbFM4cHBsZk0xajVRTklVTmlYYjBWRkgza2JlNmtmVGRkWXZMako5RGhNYXNDdEhKS1hvR0RmYncifQ.Br0GWGPPcnysyUxukjBBfXNbwCAm2qlR5OYClwFF3ZQ; HttpOnly; Path=/" } ], - "headersSize": 768, + "headersSize": 618, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2020-07-29T13:10:18.064Z", - "time": 118, + "startedDateTime": "2021-01-14T13:55:44.094Z", + "time": 392, "timings": { "blocked": -1, "connect": -1, @@ -131,7 +122,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 118 + "wait": 392 } } ], diff --git a/recordings/User_3768991250/will-not-be-logged-if-has-invalid-token_1301762210/recording.har b/recordings/User_3768991250/will-not-be-logged-if-has-invalid-token_1301762210/recording.har index 8e4ea3f81..a86975194 100644 --- a/recordings/User_3768991250/will-not-be-logged-if-has-invalid-token_1301762210/recording.har +++ b/recordings/User_3768991250/will-not-be-logged-if-has-invalid-token_1301762210/recording.har @@ -8,11 +8,11 @@ }, "entries": [ { - "_id": "4836098613648775386c1e10728424dd", + "_id": "b1557b45bbbf7aed1a4a53f5141ca324", "_order": 0, "cache": {}, "request": { - "bodySize": 428, + "bodySize": 439, "cookies": [], "headers": [ { @@ -28,7 +28,7 @@ { "_fromType": "array", "name": "content-length", - "value": "428" + "value": "439" }, { "_fromType": "array", @@ -56,7 +56,7 @@ "postData": { "mimeType": "application/json", "params": [], - "text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"NotAToken\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" + "text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"NotAToken\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" }, "queryString": [], "url": "http://localhost:8000/graphql/" @@ -72,28 +72,16 @@ "headers": [ { "name": "date", - "value": "Wed, 29 Jul 2020 13:10:18 GMT" + "value": "Thu, 14 Jan 2021 14:10:40 GMT" }, { "name": "server", - "value": "WSGIServer/0.2 CPython/3.8.1" + "value": "WSGIServer/0.2 CPython/3.8.7" }, { "name": "content-type", "value": "application/json" }, - { - "name": "access-control-allow-origin", - "value": "http://localhost:9000" - }, - { - "name": "access-control-allow-methods", - "value": "POST, OPTIONS" - }, - { - "name": "access-control-allow-headers", - "value": "Origin, Content-Type, Accept, Authorization" - }, { "name": "content-length", "value": "89" @@ -101,16 +89,20 @@ { "name": "x-content-type-options", "value": "nosniff" + }, + { + "name": "referrer-policy", + "value": "same-origin" } ], - "headersSize": 334, + "headersSize": 192, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2020-07-29T13:10:18.368Z", - "time": 6, + "startedDateTime": "2021-01-14T14:10:40.611Z", + "time": 25, "timings": { "blocked": -1, "connect": -1, @@ -118,7 +110,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 6 + "wait": 25 } } ], diff --git a/recordings/User_3768991250/will-not-be-logged-if-is-non-staff_821531868/recording.har b/recordings/User_3768991250/will-not-be-logged-if-is-non-staff_821531868/recording.har new file mode 100644 index 000000000..6cc007de4 --- /dev/null +++ b/recordings/User_3768991250/will-not-be-logged-if-is-non-staff_821531868/recording.har @@ -0,0 +1,132 @@ +{ + "log": { + "_recordingName": "User/will not be logged if is non-staff", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "5.0.0" + }, + "entries": [ + { + "_id": "0b09ec35ecae5b17a2ccda062b1d6ef5", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 602, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "accept", + "value": "*/*" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-length", + "value": "602" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "_fromType": "array", + "name": "connection", + "value": "close" + }, + { + "name": "host", + "value": "localhost:8000" + } + ], + "headersSize": 254, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"client@example.com\",\"password\":\"password\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" + }, + "queryString": [], + "url": "http://localhost:8000/graphql/" + }, + "response": { + "bodySize": 616, + "content": { + "mimeType": "application/json", + "size": 616, + "text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"Gac5v8mZt6dW0HBXp5RNt8GAWciTbVzsycpqtUKV797npCXajke5h9VoF4l9MreP\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2OTg2MzMsImV4cCI6MTYxMDY5ODkzMywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJhY2Nlc3MiLCJ1c2VyX2lkIjoiVlhObGNqb3pNQT09IiwiaXNfc3RhZmYiOmZhbHNlfQ.RVYwqQSPEZoi2E_ImC30Ml37RJ2Fu6AnSmfDkAYMcqY\", \"user\": {\"id\": \"VXNlcjozMA==\", \"email\": \"client@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": false, \"userPermissions\": [], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]" + }, + "cookies": [ + { + "httpOnly": true, + "name": "refreshToken", + "path": "/", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2OTg2MzMsImV4cCI6MTYxMzI5MDYzMywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJyZWZyZXNoIiwidXNlcl9pZCI6IlZYTmxjam96TUE9PSIsImlzX3N0YWZmIjpmYWxzZSwiY3NyZlRva2VuIjoiR2FjNXY4bVp0NmRXMEhCWHA1Uk50OEdBV2NpVGJWenN5Y3BxdFVLVjc5N25wQ1hhamtlNWg5Vm9GNGw5TXJlUCJ9.jUF_9vvtwT8EUbQ4GM7u0YVivk7TiSoSecHDZ0jJ2MI" + } + ], + "headers": [ + { + "name": "date", + "value": "Fri, 15 Jan 2021 08:17:13 GMT" + }, + { + "name": "server", + "value": "WSGIServer/0.2 CPython/3.8.7" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "content-length", + "value": "616" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "referrer-policy", + "value": "same-origin" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2OTg2MzMsImV4cCI6MTYxMzI5MDYzMywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJyZWZyZXNoIiwidXNlcl9pZCI6IlZYTmxjam96TUE9PSIsImlzX3N0YWZmIjpmYWxzZSwiY3NyZlRva2VuIjoiR2FjNXY4bVp0NmRXMEhCWHA1Uk50OEdBV2NpVGJWenN5Y3BxdFVLVjc5N25wQ1hhamtlNWg5Vm9GNGw5TXJlUCJ9.jUF_9vvtwT8EUbQ4GM7u0YVivk7TiSoSecHDZ0jJ2MI; HttpOnly; Path=/" + } + ], + "headersSize": 619, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2021-01-15T08:17:12.850Z", + "time": 623, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 623 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/recordings/User_3768991250/will-not-be-logged-in-if-doesn-t-have-valid-credentials_3719199657/recording.har b/recordings/User_3768991250/will-not-be-logged-in-if-doesn-t-have-valid-credentials_3719199657/recording.har index 547653fee..174fede4a 100644 --- a/recordings/User_3768991250/will-not-be-logged-in-if-doesn-t-have-valid-credentials_3719199657/recording.har +++ b/recordings/User_3768991250/will-not-be-logged-in-if-doesn-t-have-valid-credentials_3719199657/recording.har @@ -8,11 +8,11 @@ }, "entries": [ { - "_id": "86487093ff8b070d496fcdc566e01adf", + "_id": "d94d7821dc951e48c410d691d7eccdef", "_order": 0, "cache": {}, "request": { - "bodySize": 603, + "bodySize": 614, "cookies": [], "headers": [ { @@ -28,7 +28,7 @@ { "_fromType": "array", "name": "content-length", - "value": "603" + "value": "614" }, { "_fromType": "array", @@ -56,7 +56,7 @@ "postData": { "mimeType": "application/json", "params": [], - "text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"NotAValidPassword123!\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" + "text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"NotAValidPassword123!\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" }, "queryString": [], "url": "http://localhost:8000/graphql/" @@ -72,28 +72,16 @@ "headers": [ { "name": "date", - "value": "Wed, 29 Jul 2020 13:10:18 GMT" + "value": "Thu, 14 Jan 2021 13:55:45 GMT" }, { "name": "server", - "value": "WSGIServer/0.2 CPython/3.8.1" + "value": "WSGIServer/0.2 CPython/3.8.7" }, { "name": "content-type", "value": "application/json" }, - { - "name": "access-control-allow-origin", - "value": "http://localhost:9000" - }, - { - "name": "access-control-allow-methods", - "value": "POST, OPTIONS" - }, - { - "name": "access-control-allow-headers", - "value": "Origin, Content-Type, Accept, Authorization" - }, { "name": "content-length", "value": "214" @@ -101,16 +89,20 @@ { "name": "x-content-type-options", "value": "nosniff" + }, + { + "name": "referrer-policy", + "value": "same-origin" } ], - "headersSize": 335, + "headersSize": 193, "httpVersion": "HTTP/1.1", "redirectURL": "", "status": 200, "statusText": "OK" }, - "startedDateTime": "2020-07-29T13:10:18.208Z", - "time": 99, + "startedDateTime": "2021-01-14T13:55:44.521Z", + "time": 1183, "timings": { "blocked": -1, "connect": -1, @@ -118,7 +110,7 @@ "receive": 0, "send": 0, "ssl": -1, - "wait": 99 + "wait": 1183 } } ], diff --git a/recordings/User_3768991250/will-not-be-logged-in-if-is-non-staff_2544500193/recording.har b/recordings/User_3768991250/will-not-be-logged-in-if-is-non-staff_2544500193/recording.har new file mode 100644 index 000000000..c9845a746 --- /dev/null +++ b/recordings/User_3768991250/will-not-be-logged-in-if-is-non-staff_2544500193/recording.har @@ -0,0 +1,132 @@ +{ + "log": { + "_recordingName": "User/will not be logged in if is non-staff", + "creator": { + "comment": "persister:fs", + "name": "Polly.JS", + "version": "5.0.0" + }, + "entries": [ + { + "_id": "0b09ec35ecae5b17a2ccda062b1d6ef5", + "_order": 0, + "cache": {}, + "request": { + "bodySize": 602, + "cookies": [], + "headers": [ + { + "_fromType": "array", + "name": "accept", + "value": "*/*" + }, + { + "_fromType": "array", + "name": "content-type", + "value": "application/json" + }, + { + "_fromType": "array", + "name": "content-length", + "value": "602" + }, + { + "_fromType": "array", + "name": "user-agent", + "value": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)" + }, + { + "_fromType": "array", + "name": "accept-encoding", + "value": "gzip,deflate" + }, + { + "_fromType": "array", + "name": "connection", + "value": "close" + }, + { + "name": "host", + "value": "localhost:8000" + } + ], + "headersSize": 254, + "httpVersion": "HTTP/1.1", + "method": "POST", + "postData": { + "mimeType": "application/json", + "params": [], + "text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"client@example.com\",\"password\":\"password\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]" + }, + "queryString": [], + "url": "http://localhost:8000/graphql/" + }, + "response": { + "bodySize": 616, + "content": { + "mimeType": "application/json", + "size": 616, + "text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"ztCCq2djodzVWeaynv8ifQbIIB4nrM6HyInqcZ4xqoaIEfOOKUEhAqw9rVR8Cr8L\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTEwNjY1MTcsImV4cCI6MTYxMTA2NjgxNywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJhY2Nlc3MiLCJ1c2VyX2lkIjoiVlhObGNqb3pNQT09IiwiaXNfc3RhZmYiOmZhbHNlfQ.lXsNnIBxZCCL843TTjn84lkWpE05o88F5q811ApjdKA\", \"user\": {\"id\": \"VXNlcjozMA==\", \"email\": \"client@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": false, \"userPermissions\": [], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]" + }, + "cookies": [ + { + "httpOnly": true, + "name": "refreshToken", + "path": "/", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTEwNjY1MTcsImV4cCI6MTYxMzY1ODUxNywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJyZWZyZXNoIiwidXNlcl9pZCI6IlZYTmxjam96TUE9PSIsImlzX3N0YWZmIjpmYWxzZSwiY3NyZlRva2VuIjoienRDQ3EyZGpvZHpWV2VheW52OGlmUWJJSUI0bnJNNkh5SW5xY1o0eHFvYUlFZk9PS1VFaEFxdzlyVlI4Q3I4TCJ9.hDMEK3HFSLol7rLd9dfaSCLTWgvetNDAFsb039L9PXQ" + } + ], + "headers": [ + { + "name": "date", + "value": "Tue, 19 Jan 2021 14:28:37 GMT" + }, + { + "name": "server", + "value": "WSGIServer/0.2 CPython/3.9.1" + }, + { + "name": "content-type", + "value": "application/json" + }, + { + "name": "content-length", + "value": "616" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "referrer-policy", + "value": "same-origin" + }, + { + "_fromType": "array", + "name": "set-cookie", + "value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTEwNjY1MTcsImV4cCI6MTYxMzY1ODUxNywidG9rZW4iOiJ6MDc2QndLZkNEMmYiLCJlbWFpbCI6ImNsaWVudEBleGFtcGxlLmNvbSIsInR5cGUiOiJyZWZyZXNoIiwidXNlcl9pZCI6IlZYTmxjam96TUE9PSIsImlzX3N0YWZmIjpmYWxzZSwiY3NyZlRva2VuIjoienRDQ3EyZGpvZHpWV2VheW52OGlmUWJJSUI0bnJNNkh5SW5xY1o0eHFvYUlFZk9PS1VFaEFxdzlyVlI4Q3I4TCJ9.hDMEK3HFSLol7rLd9dfaSCLTWgvetNDAFsb039L9PXQ; HttpOnly; Path=/" + } + ], + "headersSize": 619, + "httpVersion": "HTTP/1.1", + "redirectURL": "", + "status": 200, + "statusText": "OK" + }, + "startedDateTime": "2021-01-19T14:28:37.164Z", + "time": 468, + "timings": { + "blocked": -1, + "connect": -1, + "dns": -1, + "receive": 0, + "send": 0, + "ssl": -1, + "wait": 468 + } + } + ], + "pages": [], + "version": "1.2" + } +} diff --git a/schema.graphql b/schema.graphql index 2d320ab14..2945ad6ca 100644 --- a/schema.graphql +++ b/schema.graphql @@ -393,6 +393,7 @@ type Attribute implements Node & ObjectWithMetadata { privateMetadata: [MetadataItem]! metadata: [MetadataItem]! inputType: AttributeInputTypeEnum + entityType: AttributeEntityTypeEnum name: String slug: String type: AttributeTypeEnum @@ -431,6 +432,7 @@ type AttributeCreate { input AttributeCreateInput { inputType: AttributeInputTypeEnum + entityType: AttributeEntityTypeEnum name: String! slug: String type: AttributeTypeEnum! @@ -450,6 +452,11 @@ type AttributeDelete { attribute: Attribute } +enum AttributeEntityTypeEnum { + PAGE + PRODUCT +} + type AttributeError { field: String message: String @@ -490,6 +497,7 @@ enum AttributeInputTypeEnum { DROPDOWN MULTISELECT FILE + REFERENCE } type AttributeReorderValues { @@ -563,9 +571,9 @@ type AttributeValue implements Node { id: ID! name: String slug: String - type: AttributeValueType @deprecated(reason: "Use the `inputType` field to determine the type of attribute's value. This field will be removed after 2020-07-31.") translation(languageCode: LanguageCodeEnum!): AttributeValueTranslation inputType: AttributeInputTypeEnum + reference: ID file: File } @@ -598,6 +606,7 @@ input AttributeValueInput { values: [String] file: String contentType: String + references: [ID!] } type AttributeValueTranslatableContent implements Node { @@ -619,13 +628,6 @@ type AttributeValueTranslation implements Node { language: LanguageDisplay! } -enum AttributeValueType { - COLOR - GRADIENT - URL - STRING -} - type AttributeValueUpdate { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") attribute: Attribute @@ -643,6 +645,7 @@ type BulkProductError { message: String code: ProductErrorCode! attributes: [ID!] + values: [ID!] index: Int warehouses: [ID!] channels: [ID!] @@ -653,6 +656,7 @@ type BulkStockError { message: String code: ProductErrorCode! attributes: [ID!] + values: [ID!] index: Int } @@ -667,13 +671,13 @@ type Category implements Node & ObjectWithMetadata { seoDescription: String id: ID! name: String! - description: String! - descriptionJson: JSONString! + description: JSONString! slug: String! parent: Category level: Int! privateMetadata: [MetadataItem]! metadata: [MetadataItem]! + descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") ancestors(before: String, after: String, first: Int, last: Int): CategoryCountableConnection products(channel: String, before: String, after: String, first: Int, last: Int): ProductCountableConnection url: String @deprecated(reason: "This field will be removed after 2020-07-31.") @@ -717,8 +721,7 @@ input CategoryFilterInput { } input CategoryInput { - description: String - descriptionJson: JSONString + description: JSONString name: String slug: String seo: SeoInput @@ -743,8 +746,8 @@ type CategoryTranslatableContent implements Node { seoDescription: String id: ID! name: String! - description: String! - descriptionJson: JSONString! + description: JSONString! + descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") translation(languageCode: LanguageCodeEnum!): CategoryTranslation category: Category } @@ -760,9 +763,9 @@ type CategoryTranslation implements Node { seoDescription: String id: ID! name: String! - description: String! - descriptionJson: JSONString! + description: JSONString! language: LanguageDisplay! + descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") } type CategoryUpdate { @@ -1047,11 +1050,11 @@ type Collection implements Node & ObjectWithMetadata { seoDescription: String id: ID! name: String! - description: String! - descriptionJson: JSONString! + description: JSONString! slug: String! privateMetadata: [MetadataItem]! metadata: [MetadataItem]! + descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") products(filter: ProductFilterInput, sortBy: ProductOrder, before: String, after: String, first: Int, last: Int): ProductCountableConnection backgroundImage(size: Int): Image translation(languageCode: LanguageCodeEnum!): CollectionTranslation @@ -1082,6 +1085,7 @@ type CollectionChannelListingError { message: String code: ProductErrorCode! attributes: [ID!] + values: [ID!] channels: [ID!] } @@ -1117,8 +1121,7 @@ input CollectionCreateInput { isPublished: Boolean name: String slug: String - description: String - descriptionJson: JSONString + description: JSONString backgroundImage: Upload backgroundImageAlt: String seo: SeoInput @@ -1160,8 +1163,7 @@ input CollectionInput { isPublished: Boolean name: String slug: String - description: String - descriptionJson: JSONString + description: JSONString backgroundImage: Upload backgroundImageAlt: String seo: SeoInput @@ -1203,8 +1205,8 @@ type CollectionTranslatableContent implements Node { seoDescription: String id: ID! name: String! - description: String! - descriptionJson: JSONString! + description: JSONString! + descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") translation(languageCode: LanguageCodeEnum!): CollectionTranslation collection: Collection } @@ -1220,9 +1222,9 @@ type CollectionTranslation implements Node { seoDescription: String id: ID! name: String! - description: String! - descriptionJson: JSONString! + description: JSONString! language: LanguageDisplay! + descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") } type CollectionUpdate { @@ -1941,6 +1943,49 @@ enum ExportScope { FILTER } +type ExternalAuthentication { + id: String! + name: String +} + +type ExternalAuthenticationUrl { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + authenticationData: JSONString + accountErrors: [AccountError!]! +} + +type ExternalLogout { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + logoutData: JSONString + accountErrors: [AccountError!]! +} + +type ExternalObtainAccessTokens { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + token: String + refreshToken: String + csrfToken: String + user: User + accountErrors: [AccountError!]! +} + +type ExternalRefresh { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + token: String + refreshToken: String + csrfToken: String + user: User + accountErrors: [AccountError!]! +} + +type ExternalVerify { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + user: User + isValid: Boolean! + verifyData: JSONString + accountErrors: [AccountError!]! +} + type File { url: String! contentType: String @@ -1994,9 +2039,21 @@ type FulfillmentRefundProducts { orderErrors: [OrderError!]! } +type FulfillmentReturnProducts { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + returnFulfillment: Fulfillment + replaceFulfillment: Fulfillment + order: Order + replaceOrder: Order + orderErrors: [OrderError!]! +} + enum FulfillmentStatus { FULFILLED REFUNDED + RETURNED + REPLACED + REFUNDED_AND_RETURNED CANCELED } @@ -2295,10 +2352,12 @@ type Margin { stop: Int } -type Menu implements Node { +type Menu implements Node & ObjectWithMetadata { id: ID! name: String! slug: String! + privateMetadata: [MetadataItem]! + metadata: [MetadataItem]! items: [MenuItem] } @@ -2365,7 +2424,7 @@ input MenuInput { slug: String } -type MenuItem implements Node { +type MenuItem implements Node & ObjectWithMetadata { id: ID! name: String! menu: Menu! @@ -2374,6 +2433,8 @@ type MenuItem implements Node { collection: Collection page: Page level: Int! + privateMetadata: [MetadataItem]! + metadata: [MetadataItem]! children: [MenuItem] url: String translation(languageCode: LanguageCodeEnum!): MenuItemTranslation @@ -2596,6 +2657,7 @@ type Mutation { productTypeBulkDelete(ids: [ID]!): ProductTypeBulkDelete productTypeUpdate(id: ID!, input: ProductTypeInput!): ProductTypeUpdate productTypeReorderAttributes(moves: [ReorderInput]!, productTypeId: ID!, type: ProductAttributeType!): ProductTypeReorderAttributes + productReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, productId: ID!): ProductReorderAttributeValues digitalContentCreate(input: DigitalContentUploadInput!, variantId: ID!): DigitalContentCreate digitalContentDelete(variantId: ID!): DigitalContentDelete digitalContentUpdate(input: DigitalContentInput!, variantId: ID!): DigitalContentUpdate @@ -2611,6 +2673,7 @@ type Mutation { productVariantSetDefault(productId: ID!, variantId: ID!): ProductVariantSetDefault productVariantTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): ProductVariantTranslate productVariantChannelListingUpdate(id: ID!, input: [ProductVariantChannelListingAddInput!]!): ProductVariantChannelListingUpdate + productVariantReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, variantId: ID!): ProductVariantReorderAttributeValues variantImageAssign(imageId: ID!, variantId: ID!): VariantImageAssign variantImageUnassign(imageId: ID!, variantId: ID!): VariantImageUnassign paymentCapture(amount: PositiveDecimal, paymentId: ID!): PaymentCapture @@ -2630,6 +2693,7 @@ type Mutation { pageAttributeAssign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeAssign pageAttributeUnassign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeUnassign pageTypeReorderAttributes(moves: [ReorderInput!]!, pageTypeId: ID!): PageTypeReorderAttributes + pageReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, pageId: ID!): PageReorderAttributeValues draftOrderComplete(id: ID!): DraftOrderComplete draftOrderCreate(input: DraftOrderCreateInput!): DraftOrderCreate draftOrderDelete(id: ID!): DraftOrderDelete @@ -2647,6 +2711,7 @@ type Mutation { orderFulfillmentCancel(id: ID!, input: FulfillmentCancelInput!): FulfillmentCancel orderFulfillmentUpdateTracking(id: ID!, input: FulfillmentUpdateTrackingInput!): FulfillmentUpdateTracking orderFulfillmentRefundProducts(input: OrderRefundProductsInput!, order: ID!): FulfillmentRefundProducts + orderFulfillmentReturnProducts(input: OrderReturnProductsInput!, order: ID!): FulfillmentReturnProducts orderMarkAsPaid(id: ID!, transactionReference: String): OrderMarkAsPaid orderRefund(amount: PositiveDecimal!, id: ID!): OrderRefund orderUpdate(id: ID!, input: OrderUpdateInput!): OrderUpdate @@ -2743,6 +2808,11 @@ type Mutation { tokenRefresh(csrfToken: String, refreshToken: String): RefreshToken tokenVerify(token: String!): VerifyToken tokensDeactivateAll: DeactivateAllUserTokens + externalAuthenticationUrl(input: JSONString!, pluginId: String!): ExternalAuthenticationUrl + externalObtainAccessTokens(input: JSONString!, pluginId: String!): ExternalObtainAccessTokens + externalRefresh(input: JSONString!, pluginId: String!): ExternalRefresh + externalLogout(input: JSONString!, pluginId: String!): ExternalLogout + externalVerify(input: JSONString!, pluginId: String!): ExternalVerify requestPasswordReset(email: String!, redirectUrl: String!): RequestPasswordReset confirmAccount(email: String!, token: String!): ConfirmAccount setPassword(email: String!, password: String!, token: String!): SetPassword @@ -2812,7 +2882,8 @@ type Order implements Node & ObjectWithMetadata { shippingMethod: ShippingMethod shippingMethodName: String channel: Channel! - shippingPrice: TaxedMoney + shippingPrice: TaxedMoney! + shippingTaxRate: Float! token: String! voucher: Voucher giftCards: [GiftCard] @@ -2831,16 +2902,16 @@ type Order implements Node & ObjectWithMetadata { availableShippingMethods: [ShippingMethod] invoices: [Invoice] number: String - isPaid: Boolean - paymentStatus: PaymentChargeStatusEnum - paymentStatusDisplay: String + isPaid: Boolean! + paymentStatus: PaymentChargeStatusEnum! + paymentStatusDisplay: String! payments: [Payment] - total: TaxedMoney - subtotal: TaxedMoney + total: TaxedMoney! + subtotal: TaxedMoney! statusDisplay: String canFinalize: Boolean! - totalAuthorized: Money - totalCaptured: Money + totalAuthorized: Money! + totalCaptured: Money! events: [OrderEvent] totalBalance: Money! userEmail: String @@ -2909,6 +2980,7 @@ input OrderDraftFilterInput { customer: String created: DateRangeInput search: String + channels: [ID] } type OrderError { @@ -2944,8 +3016,7 @@ enum OrderErrorCode { UNIQUE VOID_INACTIVE_PAYMENT ZERO_QUANTITY - INVALID_REFUND_QUANTITY - CANNOT_REFUND_FULFILLMENT_LINE + INVALID_QUANTITY INSUFFICIENT_STOCK DUPLICATED_INPUT_ITEM NOT_AVAILABLE_IN_CHANNEL @@ -2973,6 +3044,7 @@ type OrderEvent implements Node { warehouse: Warehouse transactionReference: String shippingCostsIncluded: Boolean + relatedOrder: Order } type OrderEventCountableConnection { @@ -3006,6 +3078,7 @@ enum OrderEventsEmailsEnum { enum OrderEventsEnum { DRAFT_CREATED + DRAFT_CREATED_FROM_REPLACE DRAFT_ADDED_PRODUCTS DRAFT_REMOVED_PRODUCTS PLACED @@ -3014,6 +3087,7 @@ enum OrderEventsEnum { CANCELED ORDER_MARKED_AS_PAID ORDER_FULLY_PAID + ORDER_REPLACEMENT_CREATED UPDATED_ADDRESS EMAIL_SENT CONFIRMED @@ -3031,6 +3105,8 @@ enum OrderEventsEnum { FULFILLMENT_RESTOCKED_ITEMS FULFILLMENT_FULFILLED_ITEMS FULFILLMENT_REFUNDED + FULFILLMENT_RETURNED + FULFILLMENT_REPLACED TRACKING_UPDATED NOTE_ADDED OTHER @@ -3078,8 +3154,8 @@ type OrderLine implements Node { taxRate: Float! digitalContentUrl: DigitalContentUrl thumbnail(size: Int): Image - unitPrice: TaxedMoney - totalPrice: TaxedMoney + unitPrice: TaxedMoney! + totalPrice: TaxedMoney! variant: ProductVariant translatedProductName: String! translatedVariantName: String! @@ -3124,6 +3200,26 @@ input OrderRefundProductsInput { includeShippingCosts: Boolean = false } +input OrderReturnFulfillmentLineInput { + fulfillmentLineId: ID! + quantity: Int! + replace: Boolean = false +} + +input OrderReturnLineInput { + orderLineId: ID! + quantity: Int! + replace: Boolean = false +} + +input OrderReturnProductsInput { + orderLines: [OrderReturnLineInput!] + fulfillmentLines: [OrderReturnFulfillmentLineInput!] + amountToRefund: PositiveDecimal + includeShippingCosts: Boolean = false + refund: Boolean = false +} + type OrderSettings { automaticallyConfirmAllNewOrders: Boolean! } @@ -3166,6 +3262,8 @@ enum OrderStatus { UNCONFIRMED UNFULFILLED PARTIALLY_FULFILLED + PARTIALLY_RETURNED + RETURNED FULFILLED CANCELED } @@ -3213,8 +3311,7 @@ type Page implements Node & ObjectWithMetadata { seoDescription: String id: ID! title: String! - content: String! - contentJson: JSONString! + content: JSONString! publicationDate: Date isPublished: Boolean! slug: String! @@ -3222,6 +3319,7 @@ type Page implements Node & ObjectWithMetadata { created: DateTime! privateMetadata: [MetadataItem]! metadata: [MetadataItem]! + contentJson: String! @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.") translation(languageCode: LanguageCodeEnum!): PageTranslation attributes: [SelectedAttribute!]! } @@ -3270,8 +3368,7 @@ type PageCreate { input PageCreateInput { slug: String title: String - content: String - contentJson: JSONString + content: JSONString attributes: [AttributeValueInput!] isPublished: Boolean publicationDate: String @@ -3290,6 +3387,7 @@ type PageError { message: String code: PageErrorCode! attributes: [ID!] + values: [ID!] } enum PageErrorCode { @@ -3316,14 +3414,19 @@ type PageInfo { input PageInput { slug: String title: String - content: String - contentJson: JSONString + content: JSONString attributes: [AttributeValueInput!] isPublished: Boolean publicationDate: String seo: SeoInput } +type PageReorderAttributeValues { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + page: Page + pageErrors: [PageError!]! +} + enum PageSortField { TITLE SLUG @@ -3342,8 +3445,8 @@ type PageTranslatableContent implements Node { seoDescription: String id: ID! title: String! - content: String! - contentJson: JSONString! + content: JSONString! + contentJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.") translation(languageCode: LanguageCodeEnum!): PageTranslation page: Page } @@ -3359,17 +3462,16 @@ type PageTranslation implements Node { seoDescription: String id: ID! title: String! - content: String! - contentJson: JSONString! + content: JSONString! language: LanguageDisplay! + contentJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.") } input PageTranslationInput { seoTitle: String seoDescription: String title: String - content: String - contentJson: JSONString + content: JSONString } type PageType implements Node & ObjectWithMetadata { @@ -3737,8 +3839,7 @@ type Product implements Node & ObjectWithMetadata { seoTitle: String seoDescription: String name: String! - description: String! - descriptionJson: JSONString! + description: JSONString! productType: ProductType! slug: String! category: Category @@ -3749,6 +3850,7 @@ type Product implements Node & ObjectWithMetadata { rating: Float privateMetadata: [MetadataItem]! metadata: [MetadataItem]! + descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") url: String! @deprecated(reason: "This field will be removed after 2020-07-31.") thumbnail(size: Int): Image pricing: ProductPricingInfo @@ -3821,6 +3923,7 @@ type ProductChannelListingError { message: String code: ProductErrorCode! attributes: [ID!] + values: [ID!] channels: [ID!] } @@ -3857,8 +3960,7 @@ input ProductCreateInput { category: ID chargeTaxes: Boolean collections: [ID] - description: String - descriptionJson: JSONString + description: JSONString name: String slug: String taxCode: String @@ -3879,6 +3981,7 @@ type ProductError { message: String code: ProductErrorCode! attributes: [ID!] + values: [ID!] } enum ProductErrorCode { @@ -3988,8 +4091,7 @@ input ProductInput { category: ID chargeTaxes: Boolean collections: [ID] - description: String - descriptionJson: JSONString + description: JSONString name: String slug: String taxCode: String @@ -4026,6 +4128,12 @@ type ProductPricingInfo { priceRangeLocalCurrency: TaxedMoneyRange } +type ProductReorderAttributeValues { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + product: Product + productErrors: [ProductError!]! +} + input ProductStockFilterInput { warehouseIds: [ID!] quantity: IntRangeInput @@ -4036,8 +4144,8 @@ type ProductTranslatableContent implements Node { seoTitle: String seoDescription: String name: String! - description: String! - descriptionJson: JSONString! + description: JSONString! + descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") translation(languageCode: LanguageCodeEnum!): ProductTranslation product: Product } @@ -4053,9 +4161,9 @@ type ProductTranslation implements Node { seoTitle: String seoDescription: String name: String! - description: String! - descriptionJson: JSONString! + description: JSONString! language: LanguageDisplay! + descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.") } type ProductType implements Node & ObjectWithMetadata { @@ -4278,6 +4386,12 @@ type ProductVariantReorder { productErrors: [ProductError!]! } +type ProductVariantReorderAttributeValues { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + productVariant: ProductVariant + productErrors: [ProductError!]! +} + type ProductVariantSetDefault { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") product: Product @@ -4405,7 +4519,7 @@ type Query { permissionGroup(id: ID!): Group me: User staffUsers(filter: StaffUserInput, sortBy: UserSortingInput, before: String, after: String, first: Int, last: Int): UserCountableConnection - user(id: ID!): User + user(id: ID, email: String): User _entities(representations: [_Any]): [_Entity] _service: _Service } @@ -4840,6 +4954,7 @@ input ShippingZoneUpdateInput { type Shop { availablePaymentGateways(currency: String): [PaymentGateway!]! + availableExternalAuthentications: [ExternalAuthentication!]! availableShippingMethods(channel: String!, address: AddressInput): [ShippingMethod] geolocalization: Geolocalization countries(languageCode: LanguageCodeEnum): [CountryDisplay!]! @@ -5215,8 +5330,7 @@ input TranslationInput { seoTitle: String seoDescription: String name: String - description: String - descriptionJson: JSONString + description: JSONString } scalar UUID @@ -5547,7 +5661,7 @@ type VoucherUpdate { voucher: Voucher } -type Warehouse implements Node { +type Warehouse implements Node & ObjectWithMetadata { id: ID! name: String! slug: String! @@ -5555,6 +5669,8 @@ type Warehouse implements Node { shippingZones(before: String, after: String, first: Int, last: Int): ShippingZoneCountableConnection! address: Address! email: String! + privateMetadata: [MetadataItem]! + metadata: [MetadataItem]! } input WarehouseAddressInput { @@ -5779,4 +5895,4 @@ union _Entity = Address | User | Group | App | ProductVariant | Product | Produc type _Service { sdl: String -} +} \ No newline at end of file diff --git a/src/apps/mutations.ts b/src/apps/mutations.ts index cc2f79976..185383caf 100644 --- a/src/apps/mutations.ts +++ b/src/apps/mutations.ts @@ -79,6 +79,7 @@ export const appDeleteFailedInstallationMutation = gql` `; export const appFetchMutation = gql` + ${appErrorFragment} mutation AppFetch($manifestUrl: String!) { appFetchManifest(manifestUrl: $manifestUrl) { manifest { diff --git a/src/attributes/components/AttributeDetails/AttributeDetails.tsx b/src/attributes/components/AttributeDetails/AttributeDetails.tsx index 8ad3451d0..83f204f0e 100644 --- a/src/attributes/components/AttributeDetails/AttributeDetails.tsx +++ b/src/attributes/components/AttributeDetails/AttributeDetails.tsx @@ -1,5 +1,6 @@ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; +import { makeStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import CardTitle from "@saleor/components/CardTitle"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; @@ -7,16 +8,90 @@ import FormSpacer from "@saleor/components/FormSpacer"; import SingleSelectField from "@saleor/components/SingleSelectField"; import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import { commonMessages } from "@saleor/intl"; -import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; +import { + AttributeEntityTypeEnum, + AttributeInputTypeEnum +} from "@saleor/types/globalTypes"; import { getFormErrors } from "@saleor/utils/errors"; import getAttributeErrorMessage from "@saleor/utils/errors/attribute"; import React from "react"; -import { useIntl } from "react-intl"; +import { defineMessages, useIntl } from "react-intl"; import slugify from "slugify"; import { getAttributeSlugErrorMessage } from "../../errors"; import { AttributePageFormData } from "../AttributePage"; +const messages = defineMessages({ + attributeLabel: { + defaultMessage: "Default Label", + description: "attribute's label" + }, + attributeSlug: { + defaultMessage: "Attribute Code", + description: "attribute's slug short code label" + }, + attributeSlugHelperText: { + defaultMessage: "This is used internally. Make sure you don’t use spaces", + description: "attribute slug input field helper text" + }, + entityType: { + defaultMessage: "Entity", + description: "attribute's editor component entity" + }, + inputType: { + defaultMessage: "Catalog Input type for Store Owner", + description: "attribute's editor component" + }, + valueRequired: { + defaultMessage: "Value Required", + description: "check to require attribute to have value" + } +}); + +const inputTypeMessages = defineMessages({ + dropdown: { + defaultMessage: "Dropdown", + description: "product attribute type" + }, + file: { + defaultMessage: "File", + description: "file attribute type" + }, + multiselect: { + defaultMessage: "Multiple Select", + description: "product attribute type" + }, + references: { + defaultMessage: "References", + description: "references attribute type" + } +}); + +const entityTypeMessages = defineMessages({ + page: { + defaultMessage: "Pages", + description: "page attribute entity type" + }, + product: { + defaultMessage: "Products", + description: "product attribute entity type" + } +}); + +const useStyles = makeStyles( + theme => ({ + inputTypeSection: { + columnGap: theme.spacing(2) + "px", + display: "flex", + [theme.breakpoints.down("md")]: { + flexFlow: "wrap", + rowGap: theme.spacing(3) + "px" + } + } + }), + { name: "AttributeDetails" } +); + export interface AttributeDetailsProps { canChangeType: boolean; data: AttributePageFormData; @@ -25,39 +100,43 @@ export interface AttributeDetailsProps { onChange: (event: React.ChangeEvent) => void; } -const AttributeDetails: React.FC = ({ - canChangeType, - data, - disabled, - errors, - onChange -}) => { +const AttributeDetails: React.FC = props => { + const { canChangeType, data, disabled, errors, onChange } = props; + const classes = useStyles(props); const intl = useIntl(); const inputTypeChoices = [ { - label: intl.formatMessage({ - defaultMessage: "Dropdown", - description: "product attribute type" - }), + label: intl.formatMessage(inputTypeMessages.dropdown), value: AttributeInputTypeEnum.DROPDOWN }, { - label: intl.formatMessage({ - defaultMessage: "Multiple Select", - description: "product attribute type" - }), + label: intl.formatMessage(inputTypeMessages.multiselect), value: AttributeInputTypeEnum.MULTISELECT }, { - label: intl.formatMessage({ - defaultMessage: "File", - description: "file attribute type" - }), + label: intl.formatMessage(inputTypeMessages.file), value: AttributeInputTypeEnum.FILE + }, + { + label: intl.formatMessage(inputTypeMessages.references), + value: AttributeInputTypeEnum.REFERENCE + } + ]; + const entityTypeChoices = [ + { + label: intl.formatMessage(entityTypeMessages.page), + value: AttributeEntityTypeEnum.PAGE + }, + { + label: intl.formatMessage(entityTypeMessages.product), + value: AttributeEntityTypeEnum.PRODUCT } ]; - const formErrors = getFormErrors(["name", "slug", "inputType"], errors); + const formErrors = getFormErrors( + ["name", "slug", "inputType", "entityType"], + errors + ); return ( @@ -68,10 +147,7 @@ const AttributeDetails: React.FC = ({ = ({ - +
+ + {data.inputType === AttributeInputTypeEnum.REFERENCE && ( + + )} +
= ({ attribute === null ? { availableInGrid: true, + entityType: null, filterableInDashboard: true, filterableInStorefront: true, inputType: AttributeInputTypeEnum.DROPDOWN, @@ -98,6 +102,7 @@ const AttributePage: React.FC = ({ } : { availableInGrid: maybe(() => attribute.availableInGrid, true), + entityType: attribute?.entityType ?? null, filterableInDashboard: maybe( () => attribute.filterableInDashboard, true @@ -172,7 +177,9 @@ const AttributePage: React.FC = ({ errors={errors} onChange={change} /> - {data.inputType !== AttributeInputTypeEnum.FILE && ( + {ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes( + data.inputType + ) && ( <> = ({ const formErrors = getFormErrors(["storefrontSearchPosition"], errors); + const dashboardProperties = ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes( + data.inputType + ); + + const storefrontFacetedNavigationProperties = + ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes(data.inputType) && + data.type === AttributeTypeEnum.PRODUCT_TYPE; + return ( @@ -74,61 +125,48 @@ const AttributeProperties: React.FC = ({ /> */} - +
- {data.inputType !== AttributeInputTypeEnum.FILE && - data.type === AttributeTypeEnum.PRODUCT_TYPE && ( - <> - - {data.filterableInStorefront && ( - <> - - - - )} - - )} + {storefrontFacetedNavigationProperties && ( + <> + + {data.filterableInStorefront && ( + <> + + + + )} + + )} - + - + } @@ -136,14 +174,11 @@ const AttributeProperties: React.FC = ({ onChange={onChange} disabled={disabled} /> - {data.inputType !== AttributeInputTypeEnum.FILE && ( + {dashboardProperties && ( <> - +
@@ -151,12 +186,11 @@ const AttributeProperties: React.FC = ({ name={"filterableInDashboard" as keyof FormData} label={ <> - + - + } @@ -169,12 +203,9 @@ const AttributeProperties: React.FC = ({ name={"availableInGrid" as keyof FormData} label={ <> - + - + } diff --git a/src/attributes/fixtures.ts b/src/attributes/fixtures.ts index 160d45919..e0e167360 100644 --- a/src/attributes/fixtures.ts +++ b/src/attributes/fixtures.ts @@ -10,6 +10,7 @@ import { AttributeList_attributes_edges_node } from "./types/AttributeList"; export const attribute: AttributeDetailsFragment = { __typename: "Attribute" as "Attribute", availableInGrid: true, + entityType: null, filterableInDashboard: false, filterableInStorefront: true, id: "UHJvZHVjdEF0dHJpYnV0ZTo5", @@ -33,6 +34,7 @@ export const attribute: AttributeDetailsFragment = { file: null, id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI0", name: "John Doe", + reference: null, slug: "john-doe" }, { @@ -40,6 +42,7 @@ export const attribute: AttributeDetailsFragment = { file: null, id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI1", name: "Milionare Pirate", + reference: null, slug: "milionare-pirate" } ], @@ -63,6 +66,7 @@ export const attributes: Array ({ + name: value.name + })) + }; +} + +function getFileOrReferenceAttributeData( + data: AttributePageFormData, + values: AttributeValueEditDialogFormData[] +) { + return { + ...getSimpleAttributeData(data, values), + availableInGrid: undefined, + filterableInDashboard: undefined, + filterableInStorefront: undefined + }; +} + +export function getAttributeData( + data: AttributePageFormData, + values: AttributeValueEditDialogFormData[] +) { + if (ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes(data.inputType)) { + return getSimpleAttributeData(data, values); + } else { + return getFileOrReferenceAttributeData(data, values); + } +} + +export function getSelectedAttributeValues( + attribute: + | PageDetails_page_attributes + | ProductDetails_product_attributes + | SelectedVariantAttributeFragment +) { + if (attribute.attribute.inputType === AttributeInputTypeEnum.REFERENCE) { + return attribute.values.map(value => value.reference); + } + return attribute.values.map(value => value.slug); +} + export const isFileValueUnused = ( attributesWithNewFileValue: FormsetData, existingAttribute: @@ -54,6 +125,18 @@ export const mergeAttributeValueDeleteErrors = ( return errors; }, []); +export const mergeAttributeValues = ( + attributeId: string, + attributeValues: string[], + attributes: FormsetData +) => { + const attribute = attributes.find(attribute => attribute.id === attributeId); + + return attribute.value + ? [...attribute.value, ...attributeValues] + : attributeValues; +}; + export const getFileValuesToUploadFromAttributes = ( attributesWithNewFileValue: FormsetData ) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value); @@ -104,3 +187,131 @@ export const getAttributesAfterFileAttributesUpdate = ( return uploadedFileAttributes.concat(removedFileAttributes); }; + +export const getFileAttributeDisplayData = ( + attribute: AttributeInput, + attributesWithNewFileValue: FormsetData +) => { + const attributeWithNewFileValue = attributesWithNewFileValue.find( + attributeWithNewFile => attribute.id === attributeWithNewFile.id + ); + + if (attributeWithNewFileValue) { + return { + ...attribute, + value: attributeWithNewFileValue?.value?.name + ? [attributeWithNewFileValue.value.name] + : [] + }; + } + return attribute; +}; + +export const getPageReferenceAttributeDisplayData = ( + attribute: AttributeInput, + referencePages: SearchPages_search_edges_node[] +) => ({ + ...attribute, + data: { + ...attribute.data, + references: + referencePages?.length > 0 && attribute.value?.length > 0 + ? mapPagesToChoices( + attribute.value.map(value => { + const reference = referencePages.find( + reference => reference.id === value + ); + return { ...reference }; + }) + ) + : [] + } +}); + +export const getProductReferenceAttributeDisplayData = ( + attribute: AttributeInput, + referenceProducts: SearchProducts_search_edges_node[] +) => ({ + ...attribute, + data: { + ...attribute.data, + references: + referenceProducts?.length > 0 && attribute.value?.length > 0 + ? mapNodeToChoice( + attribute.value.map(value => { + const reference = referenceProducts.find( + reference => reference.id === value + ); + return { ...reference }; + }) + ) + : [] + } +}); + +export const getReferenceAttributeDisplayData = ( + attribute: AttributeInput, + referencePages: SearchPages_search_edges_node[], + referenceProducts: SearchProducts_search_edges_node[] +) => { + if (attribute.data.entityType === AttributeEntityTypeEnum.PAGE) { + return getPageReferenceAttributeDisplayData(attribute, referencePages); + } else if (attribute.data.entityType === AttributeEntityTypeEnum.PRODUCT) { + return getProductReferenceAttributeDisplayData( + attribute, + referenceProducts + ); + } +}; + +export const getAttributesDisplayData = ( + attributes: AttributeInput[], + attributesWithNewFileValue: FormsetData, + referencePages: SearchPages_search_edges_node[], + referenceProducts: SearchProducts_search_edges_node[] +) => + attributes.map(attribute => { + if (attribute.data.inputType === AttributeInputTypeEnum.REFERENCE) { + return getReferenceAttributeDisplayData( + attribute, + referencePages, + referenceProducts + ); + } + if (attribute.data.inputType === AttributeInputTypeEnum.FILE) { + return getFileAttributeDisplayData(attribute, attributesWithNewFileValue); + } + + return attribute; + }); + +export const getSelectedReferencesFromAttribute = < + Node extends SearchPages_search_edges_node | SearchProducts_search_edges_node +>( + attribute?: AttributeInput, + references?: Node[] +) => + references?.filter( + value => + !attribute?.value?.some(selectedValue => selectedValue === value.id) + ) || []; + +export const getAttributeValuesFromReferences = ( + attributeId: string, + attributes?: AttributeInput[], + referencePages?: SearchPages_search_edges_node[], + referenceProducts?: SearchProducts_search_edges_node[] +) => { + const attribute = attributes?.find(attribute => attribute.id === attributeId); + + if (attribute?.data?.entityType === AttributeEntityTypeEnum.PAGE) { + return mapPagesToChoices( + getSelectedReferencesFromAttribute(attribute, referencePages) + ); + } else if (attribute?.data?.entityType === AttributeEntityTypeEnum.PRODUCT) { + return mapNodeToChoice( + getSelectedReferencesFromAttribute(attribute, referenceProducts) + ); + } + return []; +}; diff --git a/src/pages/utils/handlers.test.ts b/src/attributes/utils/handlers.test.ts similarity index 93% rename from src/pages/utils/handlers.test.ts rename to src/attributes/utils/handlers.test.ts index 8b79dea92..b85684fa8 100644 --- a/src/pages/utils/handlers.test.ts +++ b/src/attributes/utils/handlers.test.ts @@ -1,9 +1,8 @@ +import { createAttributeMultiChangeHandler } from "@saleor/attributes/utils/handlers"; import { AttributeInputData } from "@saleor/components/Attributes"; import { FormsetData } from "@saleor/hooks/useFormset"; import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; -import { createAttributeMultiChangeHandler } from "./handlers"; - const attributes: FormsetData = [ { data: { @@ -15,6 +14,7 @@ const attributes: FormsetData = [ file: null, id: "attrv-1", name: "Attribute 1 Value 1", + reference: null, slug: "attr-1-v-1" } ] @@ -33,6 +33,7 @@ const attributes: FormsetData = [ file: null, id: "attrv-2", name: "Attribute 2 Value 1", + reference: null, slug: "attr-2-v-1" }, { @@ -40,6 +41,7 @@ const attributes: FormsetData = [ file: null, id: "attrv-3", name: "Attribute 2 Value 2", + reference: null, slug: "attr-2-v-2" }, { @@ -47,6 +49,7 @@ const attributes: FormsetData = [ file: null, id: "attrv-4", name: "Attribute 2 Value 3", + reference: null, slug: "attr-2-v-3" } ] @@ -69,6 +72,7 @@ const attributes: FormsetData = [ }, id: "gdghdgdhkkdae", name: "File First Value", + reference: null, slug: "file-first-value" } ] diff --git a/src/attributes/utils/handlers.ts b/src/attributes/utils/handlers.ts index 496fb4758..28110b2db 100644 --- a/src/attributes/utils/handlers.ts +++ b/src/attributes/utils/handlers.ts @@ -1,14 +1,24 @@ -import { AttributeInput } from "@saleor/components/Attributes"; +import { + AttributeInput, + AttributeInputData +} from "@saleor/components/Attributes"; import { FileUpload, FileUploadVariables } from "@saleor/files/types/FileUpload"; -import { FormsetData } from "@saleor/hooks/useFormset"; -import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails"; import { + FormsetAtomicData, + FormsetChange, + FormsetData +} from "@saleor/hooks/useFormset"; +import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails"; +import { FetchMoreProps, ReorderEvent } from "@saleor/types"; +import { + AttributeEntityTypeEnum, AttributeInputTypeEnum, AttributeValueInput } from "@saleor/types/globalTypes"; +import { move, toggle } from "@saleor/utils/lists"; import { MutationFetchResult } from "react-apollo"; import { @@ -17,6 +27,149 @@ import { } from "../types/AttributeValueDelete"; import { getFileValuesToUploadFromAttributes, isFileValueUnused } from "./data"; +export function createAttributeChangeHandler( + changeAttributeData: FormsetChange, + triggerChange: () => void +): FormsetChange { + return (attributeId: string, value: string) => { + triggerChange(); + changeAttributeData(attributeId, value === "" ? [] : [value]); + }; +} + +export function createAttributeMultiChangeHandler( + changeAttributeData: FormsetChange, + attributes: FormsetData, + triggerChange: () => void +): FormsetChange { + return (attributeId: string, value: string) => { + const attribute = attributes.find( + attribute => attribute.id === attributeId + ); + + const newAttributeValues = toggle( + value, + attribute.value, + (a, b) => a === b + ); + + triggerChange(); + changeAttributeData(attributeId, newAttributeValues); + }; +} + +export function createAttributeReferenceChangeHandler( + changeAttributeData: FormsetChange, + triggerChange: () => void +): FormsetChange { + return (attributeId: string, values: string[]) => { + changeAttributeData(attributeId, values); + triggerChange(); + }; +} + +export function createFetchReferencesHandler( + attributes: FormsetData, + assignReferencesAttributeId: string, + fetchReferencePages?: (data: string) => void, + fetchReferenceProducts?: (data: string) => void +) { + return (value: string) => { + const attribute = attributes?.find( + attribute => attribute.id === assignReferencesAttributeId + ); + + if (!attribute) { + return; + } + + if ( + attribute.data.entityType === AttributeEntityTypeEnum.PAGE && + fetchReferencePages + ) { + fetchReferencePages(value); + } else if ( + attribute.data.entityType === AttributeEntityTypeEnum.PRODUCT && + fetchReferenceProducts + ) { + fetchReferenceProducts(value); + } + }; +} + +export function createFetchMoreReferencesHandler( + attributes: FormsetData, + assignReferencesAttributeId: string, + fetchMoreReferencePages?: FetchMoreProps, + fetchMoreReferenceProducts?: FetchMoreProps +) { + const attribute = attributes?.find( + attribute => attribute.id === assignReferencesAttributeId + ); + + if (!attribute) { + return; + } + + if (attribute.data.entityType === AttributeEntityTypeEnum.PAGE) { + return fetchMoreReferencePages; + } else if (attribute.data.entityType === AttributeEntityTypeEnum.PRODUCT) { + return fetchMoreReferenceProducts; + } +} + +export function createAttributeFileChangeHandler( + changeAttributeData: FormsetChange, + attributesWithNewFileValue: FormsetData>, + addAttributeNewFileValue: (data: FormsetAtomicData) => void, + changeAttributeNewFileValue: FormsetChange, + triggerChange: () => void +): FormsetChange { + return (attributeId: string, value: File) => { + triggerChange(); + + const newFileValueAssigned = attributesWithNewFileValue.find( + attribute => attribute.id === attributeId + ); + + if (newFileValueAssigned) { + changeAttributeNewFileValue(attributeId, value); + } else { + addAttributeNewFileValue({ + data: null, + id: attributeId, + label: null, + value + }); + } + + changeAttributeData(attributeId, value ? [value.name] : []); + }; +} + +export function createAttributeValueReorderHandler( + changeAttributeData: FormsetChange, + attributes: FormsetData, + triggerChange: () => void +): FormsetChange { + return (attributeId: string, reorder: ReorderEvent) => { + triggerChange(); + + const attribute = attributes.find( + attribute => attribute.id === attributeId + ); + + const reorderedValues = move( + attribute.value[reorder.oldIndex], + attribute.value, + (a, b) => a === b, + reorder.newIndex + ); + + changeAttributeData(attributeId, reorderedValues); + }; +} + interface AttributesArgs { attributes: AttributeInput[]; updatedFileAttributes: AttributeValueInput[]; @@ -44,6 +197,12 @@ export const prepareAttributesInput = ({ id: attribute.id }; } + if (attribute.data.inputType === AttributeInputTypeEnum.REFERENCE) { + return { + id: attribute.id, + references: attribute.value + }; + } return { id: attribute.id, values: attribute.value[0] === "" ? [] : attribute.value diff --git a/src/attributes/views/AttributeCreate/AttributeCreate.tsx b/src/attributes/views/AttributeCreate/AttributeCreate.tsx index 7fe4e48f0..42589f312 100644 --- a/src/attributes/views/AttributeCreate/AttributeCreate.tsx +++ b/src/attributes/views/AttributeCreate/AttributeCreate.tsx @@ -1,12 +1,10 @@ +import { getAttributeData } from "@saleor/attributes/utils/data"; import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { getStringOrPlaceholder } from "@saleor/misc"; import { ReorderEvent } from "@saleor/types"; -import { - AttributeErrorCode, - AttributeInputTypeEnum -} from "@saleor/types/globalTypes"; +import { AttributeErrorCode } from "@saleor/types/globalTypes"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import { @@ -57,33 +55,6 @@ function areValuesEqual( return a.name === b.name; } -function getSimpleAttributeData( - data: AttributePageFormData, - values: AttributeValueEditDialogFormData[] -) { - return { - ...data, - metadata: undefined, - privateMetadata: undefined, - storefrontSearchPosition: parseInt(data.storefrontSearchPosition, 10), - values: values.map(value => ({ - name: value.name - })) - }; -} - -function getFileAttributeData( - data: AttributePageFormData, - values: AttributeValueEditDialogFormData[] -) { - return { - ...getSimpleAttributeData(data, values), - availableInGrid: undefined, - filterableInDashboard: undefined, - filterableInStorefront: undefined - }; -} - const AttributeDetails: React.FC = ({ params }) => { const navigate = useNavigator(); const notify = useNotifier(); @@ -145,10 +116,7 @@ const AttributeDetails: React.FC = ({ params }) => { setValues(move(values[oldIndex], values, areValuesEqual, newIndex)); const handleCreate = async (data: AttributePageFormData) => { - const input = - data.inputType === AttributeInputTypeEnum.FILE - ? getFileAttributeData(data, values) - : getSimpleAttributeData(data, values); + const input = getAttributeData(data, values); const result = await attributeCreate({ variables: { @@ -190,6 +158,7 @@ const AttributeDetails: React.FC = ({ params }) => { __typename: "AttributeValue" as "AttributeValue", file: null, id: valueIndex.toString(), + reference: null, slug: slugify(value.name).toLowerCase(), sortOrder: valueIndex, value: null, diff --git a/src/attributes/views/AttributeDetails/AttributeDetails.tsx b/src/attributes/views/AttributeDetails/AttributeDetails.tsx index acfc3deb3..02dc810d9 100644 --- a/src/attributes/views/AttributeDetails/AttributeDetails.tsx +++ b/src/attributes/views/AttributeDetails/AttributeDetails.tsx @@ -178,6 +178,7 @@ const AttributeDetails: React.FC = ({ id, params }) => { const handleUpdate = async (data: AttributePageFormData) => { const input = { ...data, + entityType: undefined, inputType: undefined, metadata: undefined, privateMetadata: undefined, diff --git a/src/auth/AuthProvider.test.ts b/src/auth/AuthProvider.test.ts index c9d82f557..7a418071d 100644 --- a/src/auth/AuthProvider.test.ts +++ b/src/auth/AuthProvider.test.ts @@ -2,7 +2,7 @@ import setupApi from "@test/api"; import { act, renderHook } from "@testing-library/react-hooks"; import ApolloClient from "apollo-client"; -import { useAuthProvider } from "./AuthProvider"; +import { useAuthProvider } from "./hooks/useAuthProvider"; import { getTokens, setAuthToken } from "./utils"; const apolloClient = setupApi(); @@ -14,18 +14,23 @@ function renderAuthProvider(apolloClient: ApolloClient) { const notify = jest.fn(); const { result } = renderHook(() => - useAuthProvider(intl as any, notify, apolloClient) + useAuthProvider({ apolloClient, intl: intl as any, notify }) ); return result; } -const credentials = { +const adminCredentials = { email: "admin@example.com", password: "admin", token: null }; +const nonStaffUserCredentials = { + email: "client@example.com", + password: "password" +}; + beforeEach(() => { localStorage.clear(); sessionStorage.clear(); @@ -36,10 +41,10 @@ describe("User", () => { const hook = renderAuthProvider(apolloClient); await act(() => - hook.current.login(credentials.email, credentials.password) + hook.current.login(adminCredentials.email, adminCredentials.password) ); - expect(hook.current.userContext.email).toBe(credentials.email); - credentials.token = getTokens().auth; + expect(hook.current.user.email).toBe(adminCredentials.email); + adminCredentials.token = getTokens().auth; done(); }); @@ -48,19 +53,33 @@ describe("User", () => { const hook = renderAuthProvider(apolloClient); await act(() => - hook.current.login(credentials.email, "NotAValidPassword123!") + hook.current.login(adminCredentials.email, "NotAValidPassword123!") ); - expect(hook.current.userContext).toBe(null); + expect(hook.current.user).toBe(null); + + done(); + }); + + it("will not be logged in if is non-staff", async done => { + const hook = renderAuthProvider(apolloClient); + + await act(() => + hook.current.login( + nonStaffUserCredentials.email, + nonStaffUserCredentials.password + ) + ); + expect(hook.current.user).toBe(undefined); done(); }); it("will be logged if has valid token", async done => { - setAuthToken(credentials.token, false); + setAuthToken(adminCredentials.token, false); const hook = renderAuthProvider(apolloClient); await act(() => hook.current.autologinPromise.current); - expect(hook.current.userContext.email).toBe(credentials.email); + expect(hook.current.user.email).toBe(adminCredentials.email); done(); }); @@ -70,7 +89,7 @@ describe("User", () => { const hook = renderAuthProvider(apolloClient); await act(() => hook.current.autologinPromise.current); - expect(hook.current.userContext).toBe(undefined); + expect(hook.current.user).toBe(undefined); done(); }); diff --git a/src/auth/AuthProvider.tsx b/src/auth/AuthProvider.tsx index ad83dcea8..974cee3f3 100644 --- a/src/auth/AuthProvider.tsx +++ b/src/auth/AuthProvider.tsx @@ -1,179 +1,11 @@ -import { IMessageContext } from "@saleor/components/messages"; -import { DEMO_MODE } from "@saleor/config"; -import { User } from "@saleor/fragments/types/User"; import useNotifier from "@saleor/hooks/useNotifier"; -import { getMutationStatus } from "@saleor/misc"; -import { - isSupported as isCredentialsManagementAPISupported, - login as loginWithCredentialsManagementAPI, - saveCredentials -} from "@saleor/utils/credentialsManagement"; -import ApolloClient from "apollo-client"; -import React, { useContext, useEffect, useRef, useState } from "react"; -import { useApolloClient, useMutation } from "react-apollo"; -import { IntlShape, useIntl } from "react-intl"; +import React, { useContext } from "react"; +import { useApolloClient } from "react-apollo"; +import { useIntl } from "react-intl"; import { UserContext } from "./"; -import { - tokenAuthMutation, - tokenRefreshMutation, - tokenVerifyMutation -} from "./mutations"; -import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken"; -import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth"; -import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken"; -import { - displayDemoMessage, - getTokens, - removeTokens, - setAuthToken, - setTokens -} from "./utils"; - -const persistToken = false; - -export function useAuthProvider( - intl: IntlShape, - notify: IMessageContext, - apolloClient: ApolloClient -) { - const [userContext, setUserContext] = useState(undefined); - const autologinPromise = useRef>(); - const refreshPromise = useRef>(); - - const logout = () => { - setUserContext(undefined); - if (isCredentialsManagementAPISupported) { - navigator.credentials.preventSilentAccess(); - } - removeTokens(); - }; - - const [tokenAuth, tokenAuthResult] = useMutation< - TokenAuth, - TokenAuthVariables - >(tokenAuthMutation, { - client: apolloClient, - onCompleted: result => { - if (result.tokenCreate.errors.length > 0) { - logout(); - } - - const user = result.tokenCreate.user; - - // FIXME: Now we set state also when auth fails and returned user is - // `null`, because the LoginView uses this `null` to display error. - setUserContext(user); - if (user) { - setTokens( - result.tokenCreate.token, - result.tokenCreate.csrfToken, - persistToken - ); - } - }, - onError: logout - }); - const [tokenRefresh] = useMutation( - tokenRefreshMutation, - { - client: apolloClient, - onError: logout - } - ); - const [tokenVerify, tokenVerifyResult] = useMutation< - VerifyToken, - VerifyTokenVariables - >(tokenVerifyMutation, { - client: apolloClient, - onCompleted: result => { - if (result.tokenVerify === null) { - logout(); - } else { - const user = result.tokenVerify?.user; - - if (!!user) { - setUserContext(user); - } - } - }, - onError: logout - }); - - const tokenAuthOpts = { - ...tokenAuthResult, - status: getMutationStatus(tokenAuthResult) - }; - const tokenVerifyOpts = { - ...tokenVerifyResult, - status: getMutationStatus(tokenVerifyResult) - }; - - const onLogin = () => { - if (DEMO_MODE) { - displayDemoMessage(intl, notify); - } - }; - - useEffect(() => { - const token = getTokens().auth; - if (!!token && !userContext) { - autologinPromise.current = tokenVerify({ variables: { token } }); - } else { - autologinPromise.current = loginWithCredentialsManagementAPI(login); - } - }, []); - - const login = async (email: string, password: string) => { - const result = await tokenAuth({ variables: { email, password } }); - - if (result && !result.data.tokenCreate.errors.length) { - if (!!onLogin) { - onLogin(); - } - saveCredentials(result.data.tokenCreate.user, password); - - return result.data.tokenCreate.user; - } - - return null; - }; - - const loginByToken = (auth: string, refresh: string, user: User) => { - setUserContext(user); - setTokens(auth, refresh, persistToken); - }; - - const refreshToken = (): Promise => { - if (!!refreshPromise.current) { - return refreshPromise.current; - } - - return new Promise(resolve => { - const token = getTokens().refresh; - - return tokenRefresh({ variables: { token } }).then(refreshData => { - if (!!refreshData.data.tokenRefresh?.token) { - setAuthToken(refreshData.data.tokenRefresh.token, persistToken); - return resolve(true); - } - - return resolve(false); - }); - }); - }; - - return { - autologinPromise, - login, - loginByToken, - logout, - refreshToken, - tokenAuthOpts, - tokenVerifyOpts, - userContext - }; -} +import { useAuthProvider } from "./hooks/useAuthProvider"; +import { getTokens } from "./utils"; interface AuthProviderProps { children: React.ReactNode; @@ -184,30 +16,10 @@ const AuthProvider: React.FC = ({ children }) => { const intl = useIntl(); const notify = useNotifier(); - const { - login, - loginByToken, - logout, - tokenAuthOpts, - refreshToken, - tokenVerifyOpts, - userContext - } = useAuthProvider(intl, notify, apolloClient); + const authProvider = useAuthProvider({ apolloClient, intl, notify }); return ( - - {children} - + {children} ); }; diff --git a/src/storybook/stories/auth/LoginLoading.tsx b/src/auth/components/LoginLoading/LoginLoading.stories.tsx similarity index 66% rename from src/storybook/stories/auth/LoginLoading.tsx rename to src/auth/components/LoginLoading/LoginLoading.stories.tsx index a3cb8fb09..8945047ff 100644 --- a/src/storybook/stories/auth/LoginLoading.tsx +++ b/src/auth/components/LoginLoading/LoginLoading.stories.tsx @@ -1,8 +1,8 @@ +import Decorator from "@saleor/storybook/Decorator"; import { storiesOf } from "@storybook/react"; import React from "react"; -import LoginLoading from "../../../auth/components/LoginLoading"; -import Decorator from "../../Decorator"; +import LoginLoading from "."; storiesOf("Views / Authentication / Verifying remembered user", module) .addDecorator(Decorator) diff --git a/src/storybook/stories/auth/LoginPage.tsx b/src/auth/components/LoginPage/LoginPage.stories.tsx similarity index 51% rename from src/storybook/stories/auth/LoginPage.tsx rename to src/auth/components/LoginPage/LoginPage.stories.tsx index 68a3d0add..a1d05ccdb 100644 --- a/src/storybook/stories/auth/LoginPage.tsx +++ b/src/auth/components/LoginPage/LoginPage.stories.tsx @@ -1,14 +1,24 @@ import { Omit } from "@material-ui/core"; +import CardDecorator from "@saleor/storybook/CardDecorator"; +import Decorator from "@saleor/storybook/Decorator"; import { storiesOf } from "@storybook/react"; import React from "react"; import LoginPage, { LoginCardProps } from "../../../auth/components/LoginPage"; -import CardDecorator from "../../CardDecorator"; -import Decorator from "../../Decorator"; const props: Omit = { - disableLoginButton: true, + disabled: false, error: false, + externalAuthentications: [ + { + __typename: "ExternalAuthentication", + id: "auth.plugin.example", + name: "Example auth plugin" + } + ], + externalError: false, + loading: false, + onExternalAuthentication: () => undefined, onPasswordRecovery: undefined, onSubmit: () => undefined }; @@ -18,4 +28,5 @@ storiesOf("Views / Authentication / Log in", module) .addDecorator(Decorator) .add("default", () => ) .add("error", () => ) - .add("loading", () => ); + .add("disabled", () => ) + .add("loading", () => ); diff --git a/src/auth/components/LoginPage/LoginPage.tsx b/src/auth/components/LoginPage/LoginPage.tsx index 700a9d0f4..1cda9ef13 100644 --- a/src/auth/components/LoginPage/LoginPage.tsx +++ b/src/auth/components/LoginPage/LoginPage.tsx @@ -1,18 +1,17 @@ import Button from "@material-ui/core/Button"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Divider from "@material-ui/core/Divider"; import { makeStyles } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; -import Form from "@saleor/components/Form"; +import { AvailableExternalAuthentications_shop_availableExternalAuthentications } from "@saleor/auth/types/AvailableExternalAuthentications"; import { FormSpacer } from "@saleor/components/FormSpacer"; -import { DEMO_MODE } from "@saleor/config"; +import { SubmitPromise } from "@saleor/hooks/useForm"; import { commonMessages } from "@saleor/intl"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -export interface FormData { - email: string; - password: string; -} +import LoginForm, { LoginFormData } from "./form"; const useStyles = makeStyles( theme => ({ @@ -23,7 +22,13 @@ const useStyles = makeStyles( link: { color: theme.palette.primary.main, cursor: "pointer", - textAlign: "center" + textDecoration: "underline" + }, + loading: { + alignItems: "center", + display: "flex", + height: "100vh", + justifyContent: "center" }, loginButton: { width: 140 @@ -43,27 +48,40 @@ const useStyles = makeStyles( export interface LoginCardProps { error: boolean; - disableLoginButton: boolean; + externalError: boolean; + disabled: boolean; + loading: boolean; + externalAuthentications?: AvailableExternalAuthentications_shop_availableExternalAuthentications[]; + onExternalAuthentication: (pluginId: string) => void; onPasswordRecovery: () => void; - onSubmit?(event: FormData); + onSubmit?: (event: LoginFormData) => SubmitPromise; } const LoginCard: React.FC = props => { - const { error, disableLoginButton, onPasswordRecovery, onSubmit } = props; + const { + error, + externalError, + disabled, + loading, + externalAuthentications = [], + onExternalAuthentication, + onPasswordRecovery, + onSubmit + } = props; const classes = useStyles(props); const intl = useIntl(); - let initialFormData = { email: "", password: "" }; - if (DEMO_MODE) { - initialFormData = { - email: "admin@example.com", - password: "admin" - }; + if (loading) { + return ( +
+ +
+ ); } return ( -
+ {({ change: handleChange, data, submit: handleSubmit }) => ( <> {error && ( @@ -73,6 +91,13 @@ const LoginCard: React.FC = props => { )} + {externalError && ( +
+ + + +
+ )} = props => { inputProps={{ "data-test": "email" }} + disabled={disabled} /> = props => { inputProps={{ "data-test": "password" }} + disabled={disabled} />
- + + + + ) + }} /> + {externalAuthentications.length > 0 && ( + <> + + + + + + + + )} + {externalAuthentications.map(externalAuthentication => ( + + + + + ))} )} - +
); }; LoginCard.displayName = "LoginCard"; diff --git a/src/auth/components/LoginPage/form.tsx b/src/auth/components/LoginPage/form.tsx new file mode 100644 index 000000000..8dbd7389d --- /dev/null +++ b/src/auth/components/LoginPage/form.tsx @@ -0,0 +1,73 @@ +import { DEMO_MODE } from "@saleor/config"; +import useForm, { FormChange, SubmitPromise } from "@saleor/hooks/useForm"; +import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; +import React from "react"; + +export interface LoginFormData { + email: string; + password: string; +} + +export interface UseLoginFormResult { + change: FormChange; + data: LoginFormData; + hasChanged: boolean; + submit: () => Promise; +} + +export interface LoginFormProps { + children: (props: UseLoginFormResult) => React.ReactNode; + onSubmit: (data: LoginFormData) => SubmitPromise; +} + +const getLoginFormData = () => { + if (DEMO_MODE) { + return { + email: "admin@example.com", + password: "admin" + }; + } + return { email: "", password: "" }; +}; + +function useLoginForm( + onSubmit: (data: LoginFormData) => SubmitPromise +): UseLoginFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const form = useForm(getLoginFormData()); + + const handleChange: FormChange = (event, cb) => { + form.change(event, cb); + triggerChange(); + }; + + const data: LoginFormData = { + ...form.data + }; + + const handleSubmit = async (data: LoginFormData) => { + const errors = await onSubmit(data); + + return errors; + }; + + const submit = async () => handleFormSubmit(data, handleSubmit, setChanged); + + return { + change: handleChange, + data, + hasChanged: changed, + submit + }; +} + +const LoginForm: React.FC = ({ children, onSubmit }) => { + const props = useLoginForm(onSubmit); + + return
{children(props)}
; +}; + +LoginForm.displayName = "LoginForm"; +export default LoginForm; diff --git a/src/auth/hooks/useAuthProvider.ts b/src/auth/hooks/useAuthProvider.ts new file mode 100644 index 000000000..ea3260469 --- /dev/null +++ b/src/auth/hooks/useAuthProvider.ts @@ -0,0 +1,58 @@ +import { IMessageContext } from "@saleor/components/messages"; +import { User } from "@saleor/fragments/types/User"; +import useLocalStorage from "@saleor/hooks/useLocalStorage"; +import ApolloClient from "apollo-client"; +import { MutableRefObject } from "react"; +import { IntlShape } from "react-intl"; + +import { useExternalAuthProvider } from "./useExternalAuthProvider"; +import { useSaleorAuthProvider } from "./useSaleorAuthProvider"; + +export interface UseAuthProvider { + logout: () => void; + tokenAuthLoading: boolean; + tokenRefresh: () => Promise; + tokenVerifyLoading: boolean; + user?: User; + autologinPromise?: MutableRefObject>; +} +export interface UseAuthProviderOpts { + intl: IntlShape; + notify: IMessageContext; + apolloClient: ApolloClient; +} + +export function useAuthProvider(opts: UseAuthProviderOpts) { + const [authPlugin, setAuthPlugin] = useLocalStorage("authPlugin", undefined); + + const saleorAuth = useSaleorAuthProvider({ + authPlugin, + setAuthPlugin, + ...opts + }); + + const externalAuth = useExternalAuthProvider({ + authPlugin, + setAuthPlugin, + ...opts + }); + + const loginAuth = { + login: saleorAuth.login, + loginByExternalPlugin: externalAuth.loginByExternalPlugin, + loginByToken: saleorAuth.loginByToken, + requestLoginByExternalPlugin: externalAuth.requestLoginByExternalPlugin + }; + + if (authPlugin) { + return { + ...externalAuth, + ...loginAuth + }; + } + + return { + ...saleorAuth, + ...loginAuth + }; +} diff --git a/src/auth/hooks/useExternalAuthProvider.ts b/src/auth/hooks/useExternalAuthProvider.ts new file mode 100644 index 000000000..72d06e4d6 --- /dev/null +++ b/src/auth/hooks/useExternalAuthProvider.ts @@ -0,0 +1,262 @@ +import { DEMO_MODE } from "@saleor/config"; +import { User } from "@saleor/fragments/types/User"; +import { SetLocalStorage } from "@saleor/hooks/useLocalStorage"; +import { commonMessages } from "@saleor/intl"; +import { getMutationStatus } from "@saleor/misc"; +import errorTracker from "@saleor/services/errorTracking"; +import { useEffect, useRef, useState } from "react"; +import { useMutation } from "react-apollo"; + +import { + externalAuthenticationUrlMutation, + externalObtainAccessTokensMutation, + externalTokenRefreshMutation, + externalTokenVerifyMutation +} from "../mutations"; +import { + ExternalAuthenticationUrl, + ExternalAuthenticationUrlVariables +} from "../types/ExternalAuthenticationUrl"; +import { + ExternalObtainAccessTokens, + ExternalObtainAccessTokens_externalObtainAccessTokens, + ExternalObtainAccessTokensVariables +} from "../types/ExternalObtainAccessTokens"; +import { + ExternalRefreshToken, + ExternalRefreshTokenVariables +} from "../types/ExternalRefreshToken"; +import { + ExternalVerifyToken, + ExternalVerifyTokenVariables +} from "../types/ExternalVerifyToken"; +import { + displayDemoMessage, + getTokens, + removeTokens, + setAuthToken, + setTokens +} from "../utils"; +import { UseAuthProvider, UseAuthProviderOpts } from "./useAuthProvider"; + +export interface RequestExternalLoginInput { + redirectUri: string; +} +export interface ExternalLoginInput { + code: string; + state: string; +} + +export interface UseExternalAuthProvider extends UseAuthProvider { + requestLoginByExternalPlugin: ( + pluginId: string, + input: RequestExternalLoginInput + ) => Promise; + loginByExternalPlugin: ( + input: ExternalLoginInput + ) => Promise; +} +export interface UseExternalAuthProviderOpts extends UseAuthProviderOpts { + setAuthPlugin: SetLocalStorage; + authPlugin: string; +} + +const persistToken = false; + +export function useExternalAuthProvider({ + apolloClient, + authPlugin, + intl, + notify, + setAuthPlugin +}: UseExternalAuthProviderOpts): UseExternalAuthProvider { + const [userContext, setUserContext] = useState(undefined); + const autologinPromise = useRef>(); + const refreshPromise = useRef>(); + + useEffect(() => { + const token = getTokens().auth; + if (authPlugin && !!token && !userContext) { + const input = JSON.stringify({ + token + }); + autologinPromise.current = tokenVerify({ + variables: { input, pluginId: authPlugin } + }); + } + }, []); + + useEffect(() => { + if (authPlugin && userContext) { + const { id, email, firstName, lastName } = userContext; + errorTracker.setUserData({ + email, + id, + username: `${firstName} ${lastName}` + }); + + if (!userContext.isStaff) { + logout(); + notify({ + status: "error", + text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess), + title: intl.formatMessage(commonMessages.insufficientPermissions) + }); + } + } + }, [userContext]); + + const logout = () => { + setUserContext(undefined); + setAuthPlugin(undefined); + removeTokens(); + }; + + const [externalAuthenticationUrl] = useMutation< + ExternalAuthenticationUrl, + ExternalAuthenticationUrlVariables + >(externalAuthenticationUrlMutation, { + client: apolloClient, + onError: logout + }); + const [obtainAccessTokens, obtainAccessTokensResult] = useMutation< + ExternalObtainAccessTokens, + ExternalObtainAccessTokensVariables + >(externalObtainAccessTokensMutation, { + client: apolloClient, + onCompleted: ({ externalObtainAccessTokens }) => { + if (externalObtainAccessTokens.errors.length > 0) { + logout(); + } + + const user = externalObtainAccessTokens.user; + + setUserContext(user); + if (user) { + setTokens( + externalObtainAccessTokens.token, + externalObtainAccessTokens.csrfToken, + persistToken + ); + } + }, + onError: logout + }); + const [tokenRefresh] = useMutation< + ExternalRefreshToken, + ExternalRefreshTokenVariables + >(externalTokenRefreshMutation, { + client: apolloClient, + onError: logout + }); + const [tokenVerify, tokenVerifyResult] = useMutation< + ExternalVerifyToken, + ExternalVerifyTokenVariables + >(externalTokenVerifyMutation, { + client: apolloClient, + onCompleted: result => { + if (result.externalVerify === null) { + logout(); + } else { + const user = result.externalVerify?.user; + + if (!!user) { + setUserContext(user); + } + } + }, + onError: logout + }); + + const obtainAccessTokensOpts = { + ...obtainAccessTokensResult, + status: getMutationStatus(obtainAccessTokensResult) + }; + const tokenVerifyOpts = { + ...tokenVerifyResult, + status: getMutationStatus(tokenVerifyResult) + }; + + const onLogin = () => { + if (DEMO_MODE) { + displayDemoMessage(intl, notify); + } + }; + + const requestLoginByExternalPlugin = async ( + pluginId: string, + pluginInput: RequestExternalLoginInput + ) => { + const input = JSON.stringify(pluginInput); + const result = await externalAuthenticationUrl({ + variables: { + input, + pluginId + } + }); + + if (result && !result.data.externalAuthenticationUrl.errors.length) { + setAuthPlugin(pluginId); + + const authenticationData = JSON.parse( + result.data.externalAuthenticationUrl.authenticationData + ); + + location.href = authenticationData.authorizationUrl; + } else { + setAuthPlugin(undefined); + } + }; + + const loginByExternalPlugin = async (loginInput: ExternalLoginInput) => { + const input = JSON.stringify(loginInput); + const result = await obtainAccessTokens({ + variables: { input, pluginId: authPlugin } + }); + + if (result && !result.data?.externalObtainAccessTokens?.errors?.length) { + if (!!onLogin) { + onLogin(); + } + } else { + setAuthPlugin(undefined); + } + + return result?.data?.externalObtainAccessTokens; + }; + + const refreshToken = (): Promise => { + if (!!refreshPromise.current) { + return refreshPromise.current; + } + + return new Promise(resolve => { + const token = getTokens().refresh; + const input = JSON.stringify({ + refreshToken: token + }); + + return tokenRefresh({ variables: { input, pluginId: authPlugin } }).then( + refreshData => { + if (!!refreshData.data.externalRefresh?.token) { + setAuthToken(refreshData.data.externalRefresh.token, persistToken); + return resolve(true); + } + + return resolve(false); + } + ); + }); + }; + + return { + autologinPromise, + loginByExternalPlugin, + logout, + requestLoginByExternalPlugin, + tokenAuthLoading: obtainAccessTokensOpts.loading, + tokenRefresh: refreshToken, + tokenVerifyLoading: tokenVerifyOpts.loading, + user: userContext + }; +} diff --git a/src/auth/hooks/useSaleorAuthProvider.ts b/src/auth/hooks/useSaleorAuthProvider.ts new file mode 100644 index 000000000..e671c2e69 --- /dev/null +++ b/src/auth/hooks/useSaleorAuthProvider.ts @@ -0,0 +1,204 @@ +import { DEMO_MODE } from "@saleor/config"; +import { User } from "@saleor/fragments/types/User"; +import { SetLocalStorage } from "@saleor/hooks/useLocalStorage"; +import { commonMessages } from "@saleor/intl"; +import { getMutationStatus } from "@saleor/misc"; +import errorTracker from "@saleor/services/errorTracking"; +import { + isSupported as isCredentialsManagementAPISupported, + login as loginWithCredentialsManagementAPI, + saveCredentials +} from "@saleor/utils/credentialsManagement"; +import { useEffect, useRef, useState } from "react"; +import { useMutation } from "react-apollo"; + +import { + tokenAuthMutation, + tokenRefreshMutation, + tokenVerifyMutation +} from "../mutations"; +import { RefreshToken, RefreshTokenVariables } from "../types/RefreshToken"; +import { + TokenAuth, + TokenAuth_tokenCreate, + TokenAuthVariables +} from "../types/TokenAuth"; +import { VerifyToken, VerifyTokenVariables } from "../types/VerifyToken"; +import { + displayDemoMessage, + getTokens, + removeTokens, + setAuthToken, + setTokens +} from "../utils"; +import { UseAuthProvider, UseAuthProviderOpts } from "./useAuthProvider"; + +export interface UseSaleorAuthProvider extends UseAuthProvider { + login: (username: string, password: string) => Promise; + loginByToken: (auth: string, csrf: string, user: User) => void; +} +export interface UseSaleorAuthProviderOpts extends UseAuthProviderOpts { + setAuthPlugin: SetLocalStorage; + authPlugin: string; +} + +const persistToken = false; + +export function useSaleorAuthProvider({ + apolloClient, + authPlugin, + intl, + notify, + setAuthPlugin +}: UseSaleorAuthProviderOpts): UseSaleorAuthProvider { + const [userContext, setUserContext] = useState(undefined); + const autologinPromise = useRef>(); + const refreshPromise = useRef>(); + + useEffect(() => { + const token = getTokens().auth; + if (!authPlugin && !!token && !userContext) { + autologinPromise.current = tokenVerify({ variables: { token } }); + } else if (!authPlugin) { + autologinPromise.current = loginWithCredentialsManagementAPI(login); + } + }, []); + + useEffect(() => { + if (!authPlugin && userContext) { + const { id, email, firstName, lastName } = userContext; + errorTracker.setUserData({ + email, + id, + username: `${firstName} ${lastName}` + }); + + if (!userContext.isStaff) { + logout(); + notify({ + status: "error", + text: intl.formatMessage(commonMessages.unauthorizedDashboardAccess), + title: intl.formatMessage(commonMessages.insufficientPermissions) + }); + } + } + }, [userContext]); + + const logout = () => { + setUserContext(undefined); + if (isCredentialsManagementAPISupported) { + navigator.credentials.preventSilentAccess(); + } + removeTokens(); + }; + + const [tokenAuth, tokenAuthResult] = useMutation< + TokenAuth, + TokenAuthVariables + >(tokenAuthMutation, { + client: apolloClient, + onCompleted: ({ tokenCreate }) => { + if (tokenCreate.errors.length > 0) { + logout(); + } + + const user = tokenCreate.user; + + setUserContext(user); + if (user) { + setTokens(tokenCreate.token, tokenCreate.csrfToken, persistToken); + } + }, + onError: logout + }); + const [tokenRefresh] = useMutation( + tokenRefreshMutation, + { + client: apolloClient, + onError: logout + } + ); + const [tokenVerify, tokenVerifyResult] = useMutation< + VerifyToken, + VerifyTokenVariables + >(tokenVerifyMutation, { + client: apolloClient, + onCompleted: result => { + if (result.tokenVerify === null) { + logout(); + } else { + const user = result.tokenVerify?.user; + + if (!!user) { + setUserContext(user); + } + } + }, + onError: logout + }); + + const tokenAuthOpts = { + ...tokenAuthResult, + status: getMutationStatus(tokenAuthResult) + }; + const tokenVerifyOpts = { + ...tokenVerifyResult, + status: getMutationStatus(tokenVerifyResult) + }; + + const onLogin = () => { + if (DEMO_MODE) { + displayDemoMessage(intl, notify); + } + }; + + const login = async (email: string, password: string) => { + setAuthPlugin(undefined); + const result = await tokenAuth({ variables: { email, password } }); + + if (result && !result.data.tokenCreate.errors.length) { + if (!!onLogin) { + onLogin(); + } + saveCredentials(result.data.tokenCreate.user, password); + } + + return result.data.tokenCreate; + }; + + const loginByToken = (auth: string, refresh: string, user: User) => { + setAuthPlugin(undefined); + setUserContext(user); + setTokens(auth, refresh, persistToken); + }; + + const refreshToken = (): Promise => { + if (!!refreshPromise.current) { + return refreshPromise.current; + } + + return new Promise(resolve => { + const token = getTokens().refresh; + + return tokenRefresh({ variables: { token } }).then(refreshData => { + if (!!refreshData.data.tokenRefresh?.token) { + setAuthToken(refreshData.data.tokenRefresh.token, persistToken); + return resolve(true); + } + + return resolve(false); + }); + }); + }; + + return { + autologinPromise, + login, + loginByToken, + logout, + tokenAuthLoading: tokenAuthOpts.loading, + tokenRefresh: refreshToken, + tokenVerifyLoading: tokenVerifyOpts.loading, + user: userContext + }; +} diff --git a/src/auth/index.tsx b/src/auth/index.tsx index 4865edc02..b46640c16 100644 --- a/src/auth/index.tsx +++ b/src/auth/index.tsx @@ -1,32 +1,57 @@ import { User } from "@saleor/fragments/types/User"; -import React from "react"; -import { Route, Switch } from "react-router-dom"; +import { parse as parseQs } from "qs"; +import React, { MutableRefObject } from "react"; +import { Route, RouteComponentProps, Switch } from "react-router-dom"; import Layout from "./components/Layout"; import { + ExternalLoginInput, + RequestExternalLoginInput +} from "./hooks/useExternalAuthProvider"; +import { ExternalObtainAccessTokens_externalObtainAccessTokens } from "./types/ExternalObtainAccessTokens"; +import { TokenAuth_tokenCreate } from "./types/TokenAuth"; +import { + LoginUrlQueryParams, newPasswordPath, passwordResetPath, passwordResetSuccessPath } from "./urls"; -import LoginView from "./views/Login"; +import LoginViewComponent from "./views/Login"; import NewPassword from "./views/NewPassword"; import ResetPassword from "./views/ResetPassword"; import ResetPasswordSuccess from "./views/ResetPasswordSuccess"; +const LoginView: React.FC> = () => { + const qs = parseQs(location.search.substr(1)); + const params: LoginUrlQueryParams = qs; + + return ; +}; + interface UserContext { - login: (username: string, password: string) => void; + login: (username: string, password: string) => Promise; + loginByExternalPlugin: ( + input: ExternalLoginInput + ) => Promise; loginByToken: (auth: string, csrf: string, user: User) => void; logout: () => void; + requestLoginByExternalPlugin: ( + pluginId: string, + input: RequestExternalLoginInput + ) => Promise; tokenAuthLoading: boolean; tokenRefresh: () => Promise; tokenVerifyLoading: boolean; user?: User; + autologinPromise?: MutableRefObject>; } export const UserContext = React.createContext({ login: undefined, + loginByExternalPlugin: undefined, loginByToken: undefined, logout: undefined, + requestLoginByExternalPlugin: undefined, tokenAuthLoading: false, tokenRefresh: undefined, tokenVerifyLoading: false diff --git a/src/auth/mutations.ts b/src/auth/mutations.ts index 8c21ce32d..226c23a37 100644 --- a/src/auth/mutations.ts +++ b/src/auth/mutations.ts @@ -82,3 +82,52 @@ export const SetPasswordMutation = TypedMutation< SetPassword, SetPasswordVariables >(setPassword); + +export const externalAuthenticationUrlMutation = gql` + ${accountErrorFragment} + mutation ExternalAuthenticationUrl($pluginId: String!, $input: JSONString!) { + externalAuthenticationUrl(pluginId: $pluginId, input: $input) { + authenticationData + errors: accountErrors { + ...AccountErrorFragment + } + } + } +`; + +export const externalObtainAccessTokensMutation = gql` + ${accountErrorFragment} + ${fragmentUser} + mutation ExternalObtainAccessTokens($pluginId: String!, $input: JSONString!) { + externalObtainAccessTokens(pluginId: $pluginId, input: $input) { + token + csrfToken + user { + ...User + } + errors: accountErrors { + ...AccountErrorFragment + } + } + } +`; + +export const externalTokenRefreshMutation = gql` + mutation ExternalRefreshToken($pluginId: String!, $input: JSONString!) { + externalRefresh(pluginId: $pluginId, input: $input) { + token + } + } +`; + +export const externalTokenVerifyMutation = gql` + ${fragmentUser} + mutation ExternalVerifyToken($pluginId: String!, $input: JSONString!) { + externalVerify(pluginId: $pluginId, input: $input) { + verifyData + user { + ...User + } + } + } +`; diff --git a/src/auth/queries.ts b/src/auth/queries.ts new file mode 100644 index 000000000..18304273e --- /dev/null +++ b/src/auth/queries.ts @@ -0,0 +1,12 @@ +import gql from "graphql-tag"; + +export const availableExternalAuthentications = gql` + query AvailableExternalAuthentications { + shop { + availableExternalAuthentications { + id + name + } + } + } +`; diff --git a/src/auth/types/AvailableExternalAuthentications.ts b/src/auth/types/AvailableExternalAuthentications.ts new file mode 100644 index 000000000..886830ab0 --- /dev/null +++ b/src/auth/types/AvailableExternalAuthentications.ts @@ -0,0 +1,22 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: AvailableExternalAuthentications +// ==================================================== + +export interface AvailableExternalAuthentications_shop_availableExternalAuthentications { + __typename: "ExternalAuthentication"; + id: string; + name: string | null; +} + +export interface AvailableExternalAuthentications_shop { + __typename: "Shop"; + availableExternalAuthentications: AvailableExternalAuthentications_shop_availableExternalAuthentications[]; +} + +export interface AvailableExternalAuthentications { + shop: AvailableExternalAuthentications_shop; +} diff --git a/src/auth/types/ExternalAuthenticationUrl.ts b/src/auth/types/ExternalAuthenticationUrl.ts new file mode 100644 index 000000000..1fb2fd723 --- /dev/null +++ b/src/auth/types/ExternalAuthenticationUrl.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { AccountErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: ExternalAuthenticationUrl +// ==================================================== + +export interface ExternalAuthenticationUrl_externalAuthenticationUrl_errors { + __typename: "AccountError"; + code: AccountErrorCode; + field: string | null; +} + +export interface ExternalAuthenticationUrl_externalAuthenticationUrl { + __typename: "ExternalAuthenticationUrl"; + authenticationData: any | null; + errors: ExternalAuthenticationUrl_externalAuthenticationUrl_errors[]; +} + +export interface ExternalAuthenticationUrl { + externalAuthenticationUrl: ExternalAuthenticationUrl_externalAuthenticationUrl | null; +} + +export interface ExternalAuthenticationUrlVariables { + pluginId: string; + input: any; +} diff --git a/src/auth/types/ExternalObtainAccessTokens.ts b/src/auth/types/ExternalObtainAccessTokens.ts new file mode 100644 index 000000000..8497ab2b8 --- /dev/null +++ b/src/auth/types/ExternalObtainAccessTokens.ts @@ -0,0 +1,54 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PermissionEnum, AccountErrorCode } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: ExternalObtainAccessTokens +// ==================================================== + +export interface ExternalObtainAccessTokens_externalObtainAccessTokens_user_userPermissions { + __typename: "UserPermission"; + code: PermissionEnum; + name: string; +} + +export interface ExternalObtainAccessTokens_externalObtainAccessTokens_user_avatar { + __typename: "Image"; + url: string; +} + +export interface ExternalObtainAccessTokens_externalObtainAccessTokens_user { + __typename: "User"; + id: string; + email: string; + firstName: string; + lastName: string; + isStaff: boolean; + userPermissions: (ExternalObtainAccessTokens_externalObtainAccessTokens_user_userPermissions | null)[] | null; + avatar: ExternalObtainAccessTokens_externalObtainAccessTokens_user_avatar | null; +} + +export interface ExternalObtainAccessTokens_externalObtainAccessTokens_errors { + __typename: "AccountError"; + code: AccountErrorCode; + field: string | null; +} + +export interface ExternalObtainAccessTokens_externalObtainAccessTokens { + __typename: "ExternalObtainAccessTokens"; + token: string | null; + csrfToken: string | null; + user: ExternalObtainAccessTokens_externalObtainAccessTokens_user | null; + errors: ExternalObtainAccessTokens_externalObtainAccessTokens_errors[]; +} + +export interface ExternalObtainAccessTokens { + externalObtainAccessTokens: ExternalObtainAccessTokens_externalObtainAccessTokens | null; +} + +export interface ExternalObtainAccessTokensVariables { + pluginId: string; + input: any; +} diff --git a/src/auth/types/ExternalRefreshToken.ts b/src/auth/types/ExternalRefreshToken.ts new file mode 100644 index 000000000..2c2822403 --- /dev/null +++ b/src/auth/types/ExternalRefreshToken.ts @@ -0,0 +1,21 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: ExternalRefreshToken +// ==================================================== + +export interface ExternalRefreshToken_externalRefresh { + __typename: "ExternalRefresh"; + token: string | null; +} + +export interface ExternalRefreshToken { + externalRefresh: ExternalRefreshToken_externalRefresh | null; +} + +export interface ExternalRefreshTokenVariables { + pluginId: string; + input: any; +} diff --git a/src/auth/types/ExternalVerifyToken.ts b/src/auth/types/ExternalVerifyToken.ts new file mode 100644 index 000000000..8e2851aed --- /dev/null +++ b/src/auth/types/ExternalVerifyToken.ts @@ -0,0 +1,46 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { PermissionEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: ExternalVerifyToken +// ==================================================== + +export interface ExternalVerifyToken_externalVerify_user_userPermissions { + __typename: "UserPermission"; + code: PermissionEnum; + name: string; +} + +export interface ExternalVerifyToken_externalVerify_user_avatar { + __typename: "Image"; + url: string; +} + +export interface ExternalVerifyToken_externalVerify_user { + __typename: "User"; + id: string; + email: string; + firstName: string; + lastName: string; + isStaff: boolean; + userPermissions: (ExternalVerifyToken_externalVerify_user_userPermissions | null)[] | null; + avatar: ExternalVerifyToken_externalVerify_user_avatar | null; +} + +export interface ExternalVerifyToken_externalVerify { + __typename: "ExternalVerify"; + verifyData: any | null; + user: ExternalVerifyToken_externalVerify_user | null; +} + +export interface ExternalVerifyToken { + externalVerify: ExternalVerifyToken_externalVerify | null; +} + +export interface ExternalVerifyTokenVariables { + pluginId: string; + input: any; +} diff --git a/src/auth/types/SetPassword.ts b/src/auth/types/SetPassword.ts index 0ede71f38..2a1d539c8 100644 --- a/src/auth/types/SetPassword.ts +++ b/src/auth/types/SetPassword.ts @@ -31,6 +31,7 @@ export interface SetPassword_setPassword_user { email: string; firstName: string; lastName: string; + isStaff: boolean; userPermissions: (SetPassword_setPassword_user_userPermissions | null)[] | null; avatar: SetPassword_setPassword_user_avatar | null; } diff --git a/src/auth/types/TokenAuth.ts b/src/auth/types/TokenAuth.ts index 8da32e338..07272b217 100644 --- a/src/auth/types/TokenAuth.ts +++ b/src/auth/types/TokenAuth.ts @@ -31,6 +31,7 @@ export interface TokenAuth_tokenCreate_user { email: string; firstName: string; lastName: string; + isStaff: boolean; userPermissions: (TokenAuth_tokenCreate_user_userPermissions | null)[] | null; avatar: TokenAuth_tokenCreate_user_avatar | null; } diff --git a/src/auth/types/VerifyToken.ts b/src/auth/types/VerifyToken.ts index 10ece6970..06934e963 100644 --- a/src/auth/types/VerifyToken.ts +++ b/src/auth/types/VerifyToken.ts @@ -25,6 +25,7 @@ export interface VerifyToken_tokenVerify_user { email: string; firstName: string; lastName: string; + isStaff: boolean; userPermissions: (VerifyToken_tokenVerify_user_userPermissions | null)[] | null; avatar: VerifyToken_tokenVerify_user_avatar | null; } diff --git a/src/auth/urls.ts b/src/auth/urls.ts index c0ac7c68b..fc7c39e08 100644 --- a/src/auth/urls.ts +++ b/src/auth/urls.ts @@ -7,9 +7,18 @@ export const passwordResetSuccessPath = "/reset-password/success/"; export const passwordResetSuccessUrl = passwordResetSuccessPath; export const newPasswordPath = "/new-password/"; + +export const loginCallbackPath = "/login/callback/"; + export interface NewPasswordUrlQueryParams { email: string; token: string; } export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) => newPasswordPath + "?" + stringifyQs(params); + +export interface LoginOpenidconnectUrlQueryParams { + code: string; + state: string; +} +export type LoginUrlQueryParams = LoginOpenidconnectUrlQueryParams; diff --git a/src/auth/views/Login.tsx b/src/auth/views/Login.tsx index 70a7a8dda..59f7b1878 100644 --- a/src/auth/views/Login.tsx +++ b/src/auth/views/Login.tsx @@ -1,20 +1,93 @@ +import { APP_DEFAULT_URI, APP_MOUNT_URI } from "@saleor/config"; import useNavigator from "@saleor/hooks/useNavigator"; import useUser from "@saleor/hooks/useUser"; -import React from "react"; +import React, { useEffect, useState } from "react"; +import { useQuery } from "react-apollo"; +import urlJoin from "url-join"; +import useRouter from "use-react-router"; -import LoginPage, { FormData } from "../components/LoginPage"; -import { passwordResetUrl } from "../urls"; +import LoginPage from "../components/LoginPage"; +import { LoginFormData } from "../components/LoginPage/form"; +import { availableExternalAuthentications } from "../queries"; +import { AvailableExternalAuthentications } from "../types/AvailableExternalAuthentications"; +import { + loginCallbackPath, + LoginUrlQueryParams, + passwordResetUrl +} from "../urls"; -const LoginView: React.FC = () => { +interface LoginViewProps { + params: LoginUrlQueryParams; +} + +const LoginView: React.FC = ({ params }) => { const navigate = useNavigator(); - const { login, user, tokenAuthLoading } = useUser(); + const { location } = useRouter(); + const { + login, + requestLoginByExternalPlugin, + loginByExternalPlugin, + tokenAuthLoading + } = useUser(); + const [isError, setIsError] = useState(false); + const [isExternalError, setIsExternalError] = useState(false); + const { + data: externalAuthentications, + loading: externalAuthenticationsLoading + } = useQuery( + availableExternalAuthentications + ); - const handleSubmit = (data: FormData) => login(data.email, data.password); + const handleSubmit = async (data: LoginFormData) => { + const result = await login(data.email, data.password); + const errors = result?.errors || []; + + setIsExternalError(false); + setIsError(!result || errors?.length > 0); + return errors; + }; + + const handleRequestExternalAuthentication = (pluginId: string) => + requestLoginByExternalPlugin(pluginId, { + redirectUri: urlJoin( + window.location.origin, + APP_MOUNT_URI === APP_DEFAULT_URI ? "" : APP_MOUNT_URI, + loginCallbackPath + ) + }); + + const handleExternalAuthentication = async (code: string, state: string) => { + const result = await loginByExternalPlugin({ code, state }); + const errors = result?.errors || []; + + setIsError(false); + if (!result || errors?.length > 0) { + setIsExternalError(true); + } else { + navigate(APP_DEFAULT_URI); + } + return errors; + }; + + useEffect(() => { + const { code, state } = params; + const isCallbackPath = location.pathname.includes(loginCallbackPath); + + if (code && state && isCallbackPath) { + handleExternalAuthentication(code, state); + } + }, []); return ( navigate(passwordResetUrl)} onSubmit={handleSubmit} /> diff --git a/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx b/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx index d1e984b25..037426539 100644 --- a/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx +++ b/src/categories/components/CategoryDetailsForm/CategoryDetailsForm.tsx @@ -33,7 +33,7 @@ export const CategoryDetailsForm: React.FC = ({ }) => { const intl = useIntl(); - const formErrors = getFormErrors(["name", "descriptionJson"], errors); + const formErrors = getFormErrors(["name", "description"], errors); return ( @@ -59,8 +59,8 @@ export const CategoryDetailsForm: React.FC = ({ = ({ const result = await createCategory({ variables: { input: { - descriptionJson: JSON.stringify(formData.description), + description: JSON.stringify(formData.description), name: formData.name, seo: { description: formData.seoDescription, diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index 30a97ff02..e97265d89 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -188,7 +188,7 @@ export const CategoryDetails: React.FC = ({ id, input: { backgroundImageAlt: formData.backgroundImageAlt, - descriptionJson: JSON.stringify(formData.description), + description: JSON.stringify(formData.description), name: formData.name, seo: { description: formData.seoDescription, diff --git a/src/channels/components/ChannelForm/ChannelForm.tsx b/src/channels/components/ChannelForm/ChannelForm.tsx index 07fca80a3..2d362acaa 100644 --- a/src/channels/components/ChannelForm/ChannelForm.tsx +++ b/src/channels/components/ChannelForm/ChannelForm.tsx @@ -20,6 +20,7 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useStyles } from "../styles"; +import { ExtendedFormHelperTextProps } from "./types"; export interface FormData { name: string; @@ -80,6 +81,11 @@ export const ChannelForm: React.FC = ({ helperText={getChannelsErrorMessage(formErrors?.slug, intl)} disabled={disabled} fullWidth + FormHelperTextProps={ + { + "data-testid": "slug-text-input-helper-text" + } as ExtendedFormHelperTextProps + } label={intl.formatMessage({ defaultMessage: "Slug", description: "channel slug" @@ -124,8 +130,14 @@ export const ChannelForm: React.FC = ({ {!!currencyCodes ? ( ( + channelsListBase +); export const useChannelsList = makeQuery(channelsList); export const useChannelDetails = makeQuery( channelDetails diff --git a/src/channels/types/BaseChannels.ts b/src/channels/types/BaseChannels.ts new file mode 100644 index 000000000..aa43e7ac1 --- /dev/null +++ b/src/channels/types/BaseChannels.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: BaseChannels +// ==================================================== + +export interface BaseChannels_channels { + __typename: "Channel"; + id: string; + isActive: boolean; + name: string; + slug: string; + currencyCode: string; +} + +export interface BaseChannels { + channels: BaseChannels_channels[] | null; +} diff --git a/src/collections/components/CollectionDetails/CollectionDetails.tsx b/src/collections/components/CollectionDetails/CollectionDetails.tsx index 7e3db7e73..c52bd4c26 100644 --- a/src/collections/components/CollectionDetails/CollectionDetails.tsx +++ b/src/collections/components/CollectionDetails/CollectionDetails.tsx @@ -33,7 +33,7 @@ const CollectionDetails: React.FC = ({ }) => { const intl = useIntl(); - const formErrors = getFormErrors(["name", "descriptionJson"], errors); + const formErrors = getFormErrors(["name", "description"], errors); return ( @@ -57,8 +57,8 @@ const CollectionDetails: React.FC = ({ = ({ input: { backgroundImage: formData.backgroundImage.value, backgroundImageAlt: formData.backgroundImageAlt, - descriptionJson: JSON.stringify(formData.description), + description: JSON.stringify(formData.description), name: formData.name, seo: { description: formData.seoDescription, diff --git a/src/collections/views/CollectionDetails.tsx b/src/collections/views/CollectionDetails.tsx index 9c7425d6e..7e661ea52 100644 --- a/src/collections/views/CollectionDetails.tsx +++ b/src/collections/views/CollectionDetails.tsx @@ -198,7 +198,7 @@ export const CollectionDetails: React.FC = ({ const handleUpdate = async (formData: CollectionUpdateData) => { const input: CollectionInput = { backgroundImageAlt: formData.backgroundImageAlt, - descriptionJson: JSON.stringify(formData.description), + description: JSON.stringify(formData.description), name: formData.name, seo: { description: formData.seoDescription, diff --git a/src/components/AppHeader/AppHeader.tsx b/src/components/AppHeader/AppHeader.tsx index d38731269..686deea8e 100644 --- a/src/components/AppHeader/AppHeader.tsx +++ b/src/components/AppHeader/AppHeader.tsx @@ -61,7 +61,11 @@ const AppHeader: React.FC = props => { {anchor => anchor ? ( -
+
{children ? ( {children} diff --git a/src/components/AppLayout/AppChannelContext.tsx b/src/components/AppLayout/AppChannelContext.tsx index 930cacfd0..3b479efe7 100644 --- a/src/components/AppLayout/AppChannelContext.tsx +++ b/src/components/AppLayout/AppChannelContext.tsx @@ -1,12 +1,12 @@ import { useAuth } from "@saleor/auth/AuthProvider"; -import { useChannelsList } from "@saleor/channels/queries"; -import { ChannelDetailsFragment } from "@saleor/fragments/types/ChannelDetailsFragment"; +import { useBaseChannelsList } from "@saleor/channels/queries"; +import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment"; import useLocalStorage from "@saleor/hooks/useLocalStorage"; import React from "react"; interface UseAppChannel { - availableChannels: ChannelDetailsFragment[]; - channel: ChannelDetailsFragment; + availableChannels: ChannelFragment[]; + channel: ChannelFragment; isPickerActive: boolean; refreshChannels: () => void; setChannel: (id: string) => void; @@ -27,14 +27,14 @@ const AppChannelContext = React.createContext({ export const AppChannelProvider: React.FC = ({ children }) => { const { isAuthenticated } = useAuth(); const [selectedChannel, setSelectedChannel] = useLocalStorage("channel", ""); - const { data: channelData, refetch } = useChannelsList({ + const { data: channelData, refetch } = useBaseChannelsList({ skip: !isAuthenticated }); const [isPickerActive, setPickerActive] = React.useState(false); React.useEffect(() => { - if (!selectedChannel) { - setSelectedChannel(channelData?.channels[0].id); + if (!selectedChannel && channelData?.channels) { + setSelectedChannel(channelData.channels[0].id); } }, [channelData]); diff --git a/src/components/AppLayout/AppChannelSelect.tsx b/src/components/AppLayout/AppChannelSelect.tsx index 944443585..0bdfeb5f3 100644 --- a/src/components/AppLayout/AppChannelSelect.tsx +++ b/src/components/AppLayout/AppChannelSelect.tsx @@ -1,5 +1,5 @@ import makeStyles from "@material-ui/core/styles/makeStyles"; -import { ChannelDetailsFragment } from "@saleor/fragments/types/ChannelDetailsFragment"; +import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment"; import { ChannelProps } from "@saleor/types"; import { mapNodeToChoice } from "@saleor/utils/maps"; import React from "react"; @@ -22,7 +22,7 @@ const useStyles = makeStyles( ); export interface AppChannelSelectProps extends ChannelProps { - channels: ChannelDetailsFragment[]; + channels: ChannelFragment[]; disabled: boolean; onChannelSelect: (id: string) => void; } diff --git a/src/components/AppLayout/AppLayout.tsx b/src/components/AppLayout/AppLayout.tsx index 4be74a99f..11ee9ce45 100644 --- a/src/components/AppLayout/AppLayout.tsx +++ b/src/components/AppLayout/AppLayout.tsx @@ -239,8 +239,11 @@ const AppLayout: React.FC = ({ children }) => {
{appState.error - ? appState.error === "unhandled" && ( - + ? appState.error.type === "unhandled" && ( + ) : children}
diff --git a/src/components/AssignAttributeValueDialog/AssignAttributeValueDialog.tsx b/src/components/AssignAttributeValueDialog/AssignAttributeValueDialog.tsx new file mode 100644 index 000000000..0a8d4523b --- /dev/null +++ b/src/components/AssignAttributeValueDialog/AssignAttributeValueDialog.tsx @@ -0,0 +1,56 @@ +import { AttributeReference } from "@saleor/attributes/utils/data"; +import React from "react"; +import { defineMessages, useIntl } from "react-intl"; + +import AssignContainerDialog, { + AssignContainerDialogProps +} from "../AssignContainerDialog"; + +const messages = defineMessages({ + header: { + defaultMessage: "Assign Attribute Value", + description: "dialog header" + }, + searchLabel: { + defaultMessage: "Search Attribute Value", + description: "label" + }, + searchPlaceholder: { + defaultMessage: "Search by value name, etc...", + description: "placeholder" + } +}); + +interface AssignAttributeValueDialogProps + extends Omit< + AssignContainerDialogProps, + "containers" | "title" | "search" | "confirmButtonState" + > { + attributeValues: AttributeReference[]; +} + +const AssignAttributeValueDialog: React.FC = ({ + attributeValues, + ...rest +}) => { + const intl = useIntl(); + + return ( + ({ + id: value.value, + name: value.label + }))} + search={{ + label: intl.formatMessage(messages.searchLabel), + placeholder: intl.formatMessage(messages.searchPlaceholder) + }} + title={intl.formatMessage(messages.header)} + confirmButtonState="default" + {...rest} + /> + ); +}; + +AssignAttributeValueDialog.displayName = "AssignAttributeValueDialog"; +export default AssignAttributeValueDialog; diff --git a/src/components/AssignAttributeValueDialog/index.ts b/src/components/AssignAttributeValueDialog/index.ts new file mode 100644 index 000000000..8dce80ff9 --- /dev/null +++ b/src/components/AssignAttributeValueDialog/index.ts @@ -0,0 +1,2 @@ +export { default } from "./AssignAttributeValueDialog"; +export * from "./AssignAttributeValueDialog"; diff --git a/src/components/Attributes/Attributes.stories.tsx b/src/components/Attributes/Attributes.stories.tsx index b2f222856..679184415 100644 --- a/src/components/Attributes/Attributes.stories.tsx +++ b/src/components/Attributes/Attributes.stories.tsx @@ -12,7 +12,10 @@ const props: AttributesProps = { loading: false, onChange: () => undefined, onFileChange: () => undefined, - onMultiChange: () => undefined + onMultiChange: () => undefined, + onReferencesAddClick: () => undefined, + onReferencesRemove: () => undefined, + onReferencesReorder: () => undefined }; storiesOf("Attributes / Attributes", module) diff --git a/src/components/Attributes/Attributes.tsx b/src/components/Attributes/Attributes.tsx index 9364ec721..aada001ec 100644 --- a/src/components/Attributes/Attributes.tsx +++ b/src/components/Attributes/Attributes.tsx @@ -4,8 +4,8 @@ import IconButton from "@material-ui/core/IconButton"; import makeStyles from "@material-ui/core/styles/makeStyles"; import Typography from "@material-ui/core/Typography"; import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; +import { AttributeReference } from "@saleor/attributes/utils/data"; import CardTitle from "@saleor/components/CardTitle"; -import Grid from "@saleor/components/Grid"; import Hr from "@saleor/components/Hr"; import MultiAutocompleteSelectField, { MultiAutocompleteChoiceType @@ -17,7 +17,11 @@ import { AttributeValueFragment } from "@saleor/fragments/types/AttributeValueFr import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset"; -import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; +import { ReorderEvent } from "@saleor/types"; +import { + AttributeEntityTypeEnum, + AttributeInputTypeEnum +} from "@saleor/types/globalTypes"; import { getProductErrorMessage } from "@saleor/utils/errors"; import getPageErrorMessage from "@saleor/utils/errors/page"; import classNames from "classnames"; @@ -30,14 +34,21 @@ import { } from "react-intl"; import FileUploadField, { FileChoiceType } from "../FileUploadField"; +import SortableChipsField, { + SortableChipsFieldValueType +} from "../SortableChipsField"; +import BasicAttributeRow from "./BasicAttributeRow"; +import ExtendedAttributeRow from "./ExtendedAttributeRow"; import { VariantAttributeScope } from "./types"; export interface AttributeInputData { inputType: AttributeInputTypeEnum; + entityType?: AttributeEntityTypeEnum; variantAttributeScope?: VariantAttributeScope; isRequired: boolean; values: AttributeValueFragment[]; selectedValues?: AttributeValueFragment[]; + references?: AttributeReference[]; } export type AttributeInput = FormsetAtomicData; export type AttributeFileInput = FormsetAtomicData; @@ -49,9 +60,12 @@ export interface AttributesProps { ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment >; title?: React.ReactNode; - onChange: FormsetChange; - onMultiChange: FormsetChange; - onFileChange?: FormsetChange; // TODO: temporairy optional, should be changed to required, after all pages implement it + onChange: FormsetChange; + onMultiChange: FormsetChange; + onFileChange: FormsetChange; + onReferencesRemove: FormsetChange; + onReferencesAddClick: (attribute: AttributeInput) => void; + onReferencesReorder: FormsetChange; } const useStyles = makeStyles( @@ -123,6 +137,10 @@ function getMultiChoices( function getMultiDisplayValue( attribute: AttributeInput ): MultiAutocompleteChoiceType[] { + if (!attribute.value) { + return []; + } + return attribute.value.map(attributeValue => { const definedAttributeValue = attribute.data.values.find( definedValue => definedValue.slug === attributeValue @@ -141,6 +159,40 @@ function getMultiDisplayValue( }); } +function getReferenceDisplayValue( + attribute: AttributeInput +): SortableChipsFieldValueType[] { + if (!attribute.value) { + return []; + } + + return attribute.value.map(attributeValue => { + const definedAttributeValue = attribute.data.values.find( + definedValue => definedValue.reference === attributeValue + ); + // If value has been previously assigned, use it's data + if (!!definedAttributeValue) { + return { + label: definedAttributeValue.name, + value: definedAttributeValue.reference + }; + } + + const definedAttributeReference = attribute.data.references?.find( + reference => reference.value === attributeValue + ); + // If value has not been yet assigned, use data of reference + if (!!definedAttributeReference) { + return definedAttributeReference; + } + + return { + label: attributeValue, + value: attributeValue + }; + }); +} + function getSingleChoices( values: AttributeValueFragment[] ): SingleAutocompleteChoiceType[] { @@ -151,7 +203,7 @@ function getSingleChoices( } function getFileChoice(attribute: AttributeInput): FileChoiceType { - const attributeValue = attribute.value[0]; + const attributeValue = attribute.value?.length > 0 && attribute.value[0]; const definedAttributeValue = attribute.data.values.find( definedValue => definedValue.slug === attributeValue @@ -210,7 +262,10 @@ const Attributes: React.FC = ({ title, onChange, onMultiChange, - onFileChange + onFileChange, + onReferencesRemove, + onReferencesAddClick, + onReferencesReorder }) => { const intl = useIntl(); const classes = useStyles({}); @@ -255,76 +310,95 @@ const Attributes: React.FC = ({ return ( {attributeIndex > 0 &&
} - -
onReferencesAddClick(attribute)} + disabled={disabled} > - {attribute.label} -
-
- {attribute.data.inputType === - AttributeInputTypeEnum.FILE ? ( - - onFileChange(attribute.id, file) - } - onFileDelete={() => - onFileChange(attribute.id, undefined) - } - error={!!error} - helperText={getErrorMessage(error, intl)} - inputProps={{ - name: `attribute:${attribute.label}` - }} - /> - ) : attribute.data.inputType === - AttributeInputTypeEnum.DROPDOWN ? ( - value.slug === attribute.value[0] - )?.name || - attribute.value[0] || - "" - } - emptyOption={!attribute.data.isRequired} - error={!!error} - helperText={getErrorMessage(error, intl)} - name={`attribute:${attribute.label}`} - label={intl.formatMessage(messages.valueLabel)} - value={attribute.value[0]} - onChange={event => - onChange(attribute.id, event.target.value) - } - allowCustomValues={!attribute.data.isRequired} - /> - ) : ( - - onMultiChange(attribute.id, event.target.value) - } - allowCustomValues={!attribute.data.isRequired} - /> - )} -
-
+ + onReferencesRemove( + attribute.id, + attribute.value?.filter(id => id !== value) + ) + } + onValueReorder={event => + onReferencesReorder(attribute.id, event) + } + loading={loading} + error={!!error} + helperText={getErrorMessage(error, intl)} + /> + + ) : attribute.data.inputType === + AttributeInputTypeEnum.FILE ? ( + + onFileChange(attribute.id, file)} + onFileDelete={() => + onFileChange(attribute.id, undefined) + } + error={!!error} + helperText={getErrorMessage(error, intl)} + inputProps={{ + name: `attribute:${attribute.label}` + }} + /> + + ) : attribute.data.inputType === + AttributeInputTypeEnum.DROPDOWN ? ( + + value.slug === attribute.value[0] + )?.name || + attribute.value[0] || + "" + } + emptyOption={!attribute.data.isRequired} + error={!!error} + helperText={getErrorMessage(error, intl)} + name={`attribute:${attribute.label}`} + label={intl.formatMessage(messages.valueLabel)} + value={attribute.value[0]} + onChange={event => + onChange(attribute.id, event.target.value) + } + allowCustomValues={!attribute.data.isRequired} + /> + + ) : ( + + + onMultiChange(attribute.id, event.target.value) + } + allowCustomValues={!attribute.data.isRequired} + /> + + )}
); })} diff --git a/src/components/Attributes/BasicAttributeRow.tsx b/src/components/Attributes/BasicAttributeRow.tsx new file mode 100644 index 000000000..ee0bb61dc --- /dev/null +++ b/src/components/Attributes/BasicAttributeRow.tsx @@ -0,0 +1,44 @@ +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import Grid from "@saleor/components/Grid"; +import React from "react"; + +const useStyles = makeStyles( + theme => ({ + attributeSection: { + "&:last-of-type": { + paddingBottom: 0 + }, + padding: theme.spacing(2, 0) + }, + attributeSectionLabel: { + alignItems: "center", + display: "flex" + } + }), + { name: "BasicAttributeRow" } +); + +interface BasicAttributeRowProps { + label: string; +} + +const BasicAttributeRow: React.FC = props => { + const { label, children } = props; + const classes = useStyles(props); + + return ( + +
+ {label} +
+
{children}
+
+ ); +}; + +BasicAttributeRow.displayName = "BasicAttributeRow"; +export default BasicAttributeRow; diff --git a/src/components/Attributes/ExtendedAttributeRow.tsx b/src/components/Attributes/ExtendedAttributeRow.tsx new file mode 100644 index 000000000..1bc9ac70b --- /dev/null +++ b/src/components/Attributes/ExtendedAttributeRow.tsx @@ -0,0 +1,65 @@ +import Button from "@material-ui/core/Button"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import Grid from "@saleor/components/Grid"; +import React from "react"; + +const useStyles = makeStyles( + theme => ({ + attributeSection: { + "&:last-of-type": { + paddingBottom: 0 + }, + padding: theme.spacing(2, 0) + }, + attributeSectionButton: { + float: "right" + }, + attributeSectionLabel: { + alignItems: "center", + display: "flex" + } + }), + { name: "ExtendedAttributeRow" } +); + +interface ExtendedAttributeRowProps { + label: string; + selectLabel: string; + disabled: boolean; + onSelect: () => void; +} + +const ExtendedAttributeRow: React.FC = props => { + const { label, selectLabel, disabled, onSelect, children } = props; + const classes = useStyles(props); + + return ( + <> + +
+ {label} +
+
+ +
+
+
{children}
+ + ); +}; + +ExtendedAttributeRow.displayName = "ExtendedAttributeRow"; +export default ExtendedAttributeRow; diff --git a/src/components/Attributes/fixtures.ts b/src/components/Attributes/fixtures.ts index 6a62b561c..6fb778150 100644 --- a/src/components/Attributes/fixtures.ts +++ b/src/components/Attributes/fixtures.ts @@ -1,4 +1,7 @@ -import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; +import { + AttributeEntityTypeEnum, + AttributeInputTypeEnum +} from "@saleor/types/globalTypes"; import { AttributeInput } from "./Attributes"; @@ -12,6 +15,7 @@ const DROPDOWN_ATTRIBUTE: AttributeInput = { file: null, id: "fdinugiffgffd", name: "Dropdown First Value", + reference: null, slug: "dropdown-first-value" }, { @@ -19,6 +23,7 @@ const DROPDOWN_ATTRIBUTE: AttributeInput = { file: null, id: "fdhfdhdihidff", name: "Dropdown Second Value", + reference: null, slug: "dropdown-second-value" } ] @@ -38,6 +43,7 @@ const MULTISELECT_ATTRIBUTE: AttributeInput = { file: null, id: "terteretregtt", name: "Multiselect First Value", + reference: null, slug: "multiselect-first-value" }, { @@ -45,6 +51,7 @@ const MULTISELECT_ATTRIBUTE: AttributeInput = { file: null, id: "tyueyryetopwr", name: "Multiselect Second Value", + reference: null, slug: "multiselect-second-value" }, { @@ -52,6 +59,7 @@ const MULTISELECT_ATTRIBUTE: AttributeInput = { file: null, id: "truiwrtweirqd", name: "Multiselect Third Value", + reference: null, slug: "multiselect-third-value" } ] @@ -75,19 +83,72 @@ const FILE_ATTRIBUTE: AttributeInput = { }, id: "gdghdgdhkkdae", name: "File First Value", + reference: null, slug: "file-first-value" } ] }, - id: "ifudbgidfsb", + id: "fguygygugyu", label: "File Attribute", value: [] }; +const REFERENCE_ATTRIBUTE: AttributeInput = { + data: { + entityType: AttributeEntityTypeEnum.PAGE, + inputType: AttributeInputTypeEnum.REFERENCE, + isRequired: true, + references: [ + { + label: "References First Value", + value: "vbnhgcvjhbvhj" + }, + { + label: "References Second Value", + value: "gucngdfdfvdvd" + }, + { + label: "References Third Value", + value: "dfdfdsfdsfdse" + } + ], + values: [ + { + __typename: "AttributeValue", + file: null, + id: "vbnhgcvjhbvhj", + name: "References First Value", + reference: null, + slug: "references-first-value" + }, + { + __typename: "AttributeValue", + file: null, + id: "gucngdfdfvdvd", + name: "References Second Value", + reference: null, + slug: "references-second-value" + }, + { + __typename: "AttributeValue", + file: null, + id: "dfdfdsfdsfdse", + name: "References Third Value", + reference: null, + slug: "references-third-value" + } + ] + }, + id: "kclsmcdsmcs", + label: "References Attribute", + value: [] +}; + export const ATTRIBUTES: AttributeInput[] = [ DROPDOWN_ATTRIBUTE, MULTISELECT_ATTRIBUTE, - FILE_ATTRIBUTE + FILE_ATTRIBUTE, + REFERENCE_ATTRIBUTE ]; export const ATTRIBUTES_SELECTED: AttributeInput[] = [ @@ -105,5 +166,13 @@ export const ATTRIBUTES_SELECTED: AttributeInput[] = [ { ...FILE_ATTRIBUTE, value: [FILE_ATTRIBUTE.data.values[0].slug] + }, + { + ...REFERENCE_ATTRIBUTE, + value: [ + REFERENCE_ATTRIBUTE.data.values[0].id, + REFERENCE_ATTRIBUTE.data.values[1].id, + REFERENCE_ATTRIBUTE.data.values[2].id + ] } ]; diff --git a/src/components/ChannelsAvailability/ChannelsAvailability.stories.tsx b/src/components/ChannelsAvailability/ChannelsAvailability.stories.tsx index 3395d2e91..262d18eaa 100644 --- a/src/components/ChannelsAvailability/ChannelsAvailability.stories.tsx +++ b/src/components/ChannelsAvailability/ChannelsAvailability.stories.tsx @@ -17,6 +17,7 @@ const user: User = { email: "email@example.com", firstName: "User", id: "123", + isStaff: true, lastName: "User", userPermissions: [ { diff --git a/src/components/ChannelsAvailability/ChannelsAvailability.tsx b/src/components/ChannelsAvailability/ChannelsAvailability.tsx index 87c1a415e..b2f36976d 100644 --- a/src/components/ChannelsAvailability/ChannelsAvailability.tsx +++ b/src/components/ChannelsAvailability/ChannelsAvailability.tsx @@ -417,7 +417,11 @@ export const ChannelsAvailability: React.FC = props = userPermissions={user?.userPermissions || []} requiredPermissions={[PermissionEnum.MANAGE_CHANNELS]} > -