Merge branch 'master' into SALEOR-1738-Tests-for-home-page

This commit is contained in:
Karolina 2021-02-11 13:53:29 +01:00 committed by GitHub
commit 68eb5309ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
330 changed files with 14422 additions and 6283 deletions

View file

@ -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
<!-- Do not remove this section. It is required to properly setup test instance.
Modify API_URI if you want test instance to use custom backend. -->
API_URI=https://master.staging.saleor.rocks/graphql/
API_URI=https://master.staging.saleor.cloud/graphql/

View file

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

View file

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

View file

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

View file

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

View file

@ -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 dont 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, youll 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 cant 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 cant 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": "Were 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": "Weve encountered a problem while refunding the products. Products 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"

132
package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -79,6 +79,7 @@ export const appDeleteFailedInstallationMutation = gql`
`;
export const appFetchMutation = gql`
${appErrorFragment}
mutation AppFetch($manifestUrl: String!) {
appFetchManifest(manifestUrl: $manifestUrl) {
manifest {

View file

@ -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 dont 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<any>) => void;
}
const AttributeDetails: React.FC<AttributeDetailsProps> = ({
canChangeType,
data,
disabled,
errors,
onChange
}) => {
const AttributeDetails: React.FC<AttributeDetailsProps> = 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 (
<Card>
@ -68,10 +147,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({
<TextField
disabled={disabled}
error={!!formErrors.name}
label={intl.formatMessage({
defaultMessage: "Default Label",
description: "attribute's label"
})}
label={intl.formatMessage(messages.attributeLabel)}
name={"name" as keyof AttributePageFormData}
fullWidth
helperText={getAttributeErrorMessage(formErrors.name, intl)}
@ -82,45 +158,46 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({
<TextField
disabled={disabled}
error={!!formErrors.slug}
label={intl.formatMessage({
defaultMessage: "Attribute Code",
description: "attribute's slug short code label"
})}
label={intl.formatMessage(messages.attributeSlug)}
name={"slug" as keyof AttributePageFormData}
placeholder={slugify(data.name).toLowerCase()}
fullWidth
helperText={
getAttributeSlugErrorMessage(formErrors.slug, intl) ||
intl.formatMessage({
defaultMessage:
"This is used internally. Make sure you dont use spaces",
description: "attribute slug input field helper text"
})
intl.formatMessage(messages.attributeSlugHelperText)
}
value={data.slug}
onChange={onChange}
/>
<FormSpacer />
<div className={classes.inputTypeSection}>
<SingleSelectField
choices={inputTypeChoices}
disabled={disabled || !canChangeType}
error={!!formErrors.inputType}
hint={getAttributeErrorMessage(formErrors.inputType, intl)}
label={intl.formatMessage({
defaultMessage: "Catalog Input type for Store Owner",
description: "attribute's editor component"
})}
label={intl.formatMessage(messages.inputType)}
name="inputType"
onChange={onChange}
value={data.inputType}
/>
{data.inputType === AttributeInputTypeEnum.REFERENCE && (
<SingleSelectField
choices={entityTypeChoices}
disabled={disabled || !canChangeType}
error={!!formErrors.entityType}
hint={getAttributeErrorMessage(formErrors.entityType, intl)}
label={intl.formatMessage(messages.entityType)}
name="entityType"
onChange={onChange}
value={data.entityType}
/>
)}
</div>
<FormSpacer />
<ControlledCheckbox
name={"valueRequired" as keyof AttributePageFormData}
label={intl.formatMessage({
defaultMessage: "Value Required",
description: "check to require attribute to have value"
})}
label={intl.formatMessage(messages.valueRequired)}
checked={data.valueRequired}
onChange={onChange}
disabled={disabled}

View file

@ -1,3 +1,4 @@
import { ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES } from "@saleor/attributes/utils/data";
import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
@ -17,6 +18,7 @@ import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc";
import { ReorderAction } from "@saleor/types";
import {
AttributeEntityTypeEnum,
AttributeInputTypeEnum,
AttributeTypeEnum
} from "@saleor/types/globalTypes";
@ -51,6 +53,7 @@ export interface AttributePageFormData extends MetadataFormData {
availableInGrid: boolean;
filterableInDashboard: boolean;
inputType: AttributeInputTypeEnum;
entityType: AttributeEntityTypeEnum;
filterableInStorefront: boolean;
name: string;
slug: string;
@ -84,6 +87,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
attribute === null
? {
availableInGrid: true,
entityType: null,
filterableInDashboard: true,
filterableInStorefront: true,
inputType: AttributeInputTypeEnum.DROPDOWN,
@ -98,6 +102,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
}
: {
availableInGrid: maybe(() => attribute.availableInGrid, true),
entityType: attribute?.entityType ?? null,
filterableInDashboard: maybe(
() => attribute.filterableInDashboard,
true
@ -172,7 +177,9 @@ const AttributePage: React.FC<AttributePageProps> = ({
errors={errors}
onChange={change}
/>
{data.inputType !== AttributeInputTypeEnum.FILE && (
{ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes(
data.inputType
) && (
<>
<CardSpacer />
<AttributeValues

View file

@ -2,6 +2,7 @@ import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES } from "@saleor/attributes/utils/data";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
@ -10,17 +11,59 @@ import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import { commonMessages } from "@saleor/intl";
import {
AttributeInputTypeEnum,
AttributeTypeEnum
} from "@saleor/types/globalTypes";
import { AttributeTypeEnum } from "@saleor/types/globalTypes";
import { getFormErrors } from "@saleor/utils/errors";
import getAttributeErrorMessage from "@saleor/utils/errors/attribute";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
import { AttributePageFormData } from "../AttributePage";
const messages = defineMessages({
availableInGrid: {
defaultMessage: "Add to Column Options",
description: "add attribute as column in product list table"
},
availableInGridCaption: {
defaultMessage:
"If enabled this attribute can be used as a column in product table.",
description: "caption"
},
dashboardPropertiesTitle: {
defaultMessage: "Dashboard Properties",
description: "attribute properties regarding dashboard"
},
filterableInDashboard: {
defaultMessage: "Use in Filtering",
description: "use attribute in filtering"
},
filterableInDashboardCaption: {
defaultMessage:
"If enabled, youll be able to use this attribute to filter products in product list.",
description: "caption"
},
filterableInStorefront: {
defaultMessage: "Use in Faceted Navigation",
description: "attribute is filterable in storefront"
},
storefrontPropertiesTitle: {
defaultMessage: "Storefront Properties",
description: "attribute properties regarding storefront"
},
storefrontSearchPosition: {
defaultMessage: "Position in faceted navigation",
description: "attribute position in storefront filters"
},
visibleInStorefront: {
defaultMessage: "Public",
description: "attribute visibility in storefront"
},
visibleInStorefrontCaption: {
defaultMessage: "If enabled, attribute will be accessible to customers.",
description: "caption"
}
});
export interface AttributePropertiesProps {
data: AttributePageFormData;
disabled: boolean;
@ -38,6 +81,14 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
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 (
<Card>
<CardTitle title={intl.formatMessage(commonMessages.properties)} />
@ -74,21 +125,14 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
/> */}
<Typography variant="subtitle1">
<FormattedMessage
defaultMessage="Storefront Properties"
description="attribute properties regarding storefront"
/>
<FormattedMessage {...messages.storefrontPropertiesTitle} />
</Typography>
<Hr />
{data.inputType !== AttributeInputTypeEnum.FILE &&
data.type === AttributeTypeEnum.PRODUCT_TYPE && (
{storefrontFacetedNavigationProperties && (
<>
<ControlledCheckbox
name={"filterableInStorefront" as keyof FormData}
label={intl.formatMessage({
defaultMessage: "Use in Faceted Navigation",
description: "attribute is filterable in storefront"
})}
label={intl.formatMessage(messages.filterableInStorefront)}
checked={data.filterableInStorefront}
onChange={onChange}
disabled={disabled}
@ -107,10 +151,7 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
name={
"storefrontSearchPosition" as keyof AttributePageFormData
}
label={intl.formatMessage({
defaultMessage: "Position in faceted navigation",
description: "attribute position in storefront filters"
})}
label={intl.formatMessage(messages.storefrontSearchPosition)}
value={data.storefrontSearchPosition}
onChange={onChange}
/>
@ -123,12 +164,9 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
name={"visibleInStorefront" as keyof FormData}
label={
<>
<FormattedMessage
defaultMessage="Public"
description="attribute visibility in storefront"
/>
<FormattedMessage {...messages.visibleInStorefront} />
<Typography variant="caption">
<FormattedMessage defaultMessage="If enabled, attribute will be accessible to customers." />
<FormattedMessage {...messages.visibleInStorefrontCaption} />
</Typography>
</>
}
@ -136,14 +174,11 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
onChange={onChange}
disabled={disabled}
/>
{data.inputType !== AttributeInputTypeEnum.FILE && (
{dashboardProperties && (
<>
<CardSpacer />
<Typography variant="subtitle1">
<FormattedMessage
defaultMessage="Dashboard Properties"
description="attribute properties regarding dashboard"
/>
<FormattedMessage {...messages.dashboardPropertiesTitle} />
</Typography>
<Hr />
<CardSpacer />
@ -151,12 +186,11 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
name={"filterableInDashboard" as keyof FormData}
label={
<>
<FormattedMessage
defaultMessage="Use in Filtering"
description="use attribute in filtering"
/>
<FormattedMessage {...messages.filterableInDashboard} />
<Typography variant="caption">
<FormattedMessage defaultMessage="If enabled, youll be able to use this attribute to filter products in product list." />
<FormattedMessage
{...messages.filterableInDashboardCaption}
/>
</Typography>
</>
}
@ -169,12 +203,9 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
name={"availableInGrid" as keyof FormData}
label={
<>
<FormattedMessage
defaultMessage="Add to Column Options"
description="add attribute as column in product list table"
/>
<FormattedMessage {...messages.availableInGrid} />
<Typography variant="caption">
<FormattedMessage defaultMessage="If enabled this attribute can be used as a column in product table." />
<FormattedMessage {...messages.availableInGridCaption} />
</Typography>
</>
}

View file

@ -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<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI0",
name: "John Doe",
reference: null,
slug: "john-doe",
sortOrder: 0,
value: ""
@ -72,6 +76,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI1",
name: "Milionare Pirate",
reference: null,
slug: "milionare-pirate",
sortOrder: 1,
value: ""
@ -95,6 +100,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE1",
name: "100g",
reference: null,
slug: "100g",
sortOrder: 0,
value: ""
@ -104,6 +110,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE2",
name: "250g",
reference: null,
slug: "250g",
sortOrder: 1,
value: ""
@ -113,6 +120,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE3",
name: "500g",
reference: null,
slug: "500g",
sortOrder: 2,
value: ""
@ -122,6 +130,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE4",
name: "1kg",
reference: null,
slug: "1kg",
sortOrder: 3,
value: ""
@ -145,6 +154,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjY=",
name: "Saleor",
reference: null,
slug: "saleor",
sortOrder: 0,
value: ""
@ -168,6 +178,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIx",
name: "100g",
reference: null,
slug: "100g",
sortOrder: 0,
value: ""
@ -177,6 +188,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIy",
name: "250g",
reference: null,
slug: "250g",
sortOrder: 1,
value: ""
@ -186,6 +198,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIz",
name: "500g",
reference: null,
slug: "500g",
sortOrder: 2,
value: ""
@ -209,6 +222,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEz",
name: "Arabica",
reference: null,
slug: "arabica",
sortOrder: 0,
value: ""
@ -218,6 +232,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE0",
name: "Robusta",
reference: null,
slug: "robusta",
sortOrder: 1,
value: ""
@ -241,6 +256,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM=",
name: "Round",
reference: null,
slug: "round",
sortOrder: 0,
value: ""
@ -250,6 +266,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjQ=",
name: "V-Neck",
reference: null,
slug: "v-neck",
sortOrder: 1,
value: ""
@ -259,6 +276,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjU=",
name: "Polo",
reference: null,
slug: "polo",
sortOrder: 2,
value: ""
@ -282,6 +300,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE=",
name: "Blue",
reference: null,
slug: "blue",
sortOrder: 0,
value: ""
@ -291,6 +310,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI=",
name: "White",
reference: null,
slug: "white",
sortOrder: 1,
value: ""
@ -314,6 +334,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMw",
name: "Soft",
reference: null,
slug: "soft",
sortOrder: 0,
value: ""
@ -323,6 +344,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMx",
name: "Hard",
reference: null,
slug: "hard",
sortOrder: 1,
value: ""
@ -332,6 +354,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMy",
name: "Middle soft",
reference: null,
slug: "middle-soft",
sortOrder: 2,
value: ""
@ -341,6 +364,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMz",
name: "Middle hard",
reference: null,
slug: "middle-hard",
sortOrder: 3,
value: ""
@ -350,6 +374,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM0",
name: "Middle",
reference: null,
slug: "middle",
sortOrder: 4,
value: ""
@ -359,6 +384,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM1",
name: "Very hard",
reference: null,
slug: "very-hard",
sortOrder: 5,
value: ""
@ -382,6 +408,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE5",
name: "Sour",
reference: null,
slug: "sour",
sortOrder: 0,
value: ""
@ -391,6 +418,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIw",
name: "Sweet",
reference: null,
slug: "sweet",
sortOrder: 1,
value: ""
@ -414,6 +442,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI4",
name: "English",
reference: null,
slug: "english",
sortOrder: 0,
value: ""
@ -423,6 +452,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI5",
name: "Pirate",
reference: null,
slug: "pirate",
sortOrder: 1,
value: ""
@ -446,6 +476,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI2",
name: "Mirumee Press",
reference: null,
slug: "mirumee-press",
sortOrder: 0,
value: ""
@ -455,6 +486,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI3",
name: "Saleor Publishing",
reference: null,
slug: "saleor-publishing",
sortOrder: 1,
value: ""
@ -478,6 +510,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjc=",
name: "XS",
reference: null,
slug: "xs",
sortOrder: 0,
value: ""
@ -487,6 +520,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjg=",
name: "S",
reference: null,
slug: "s",
sortOrder: 1,
value: ""
@ -496,6 +530,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjk=",
name: "M",
reference: null,
slug: "m",
sortOrder: 2,
value: ""
@ -505,6 +540,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEw",
name: "L",
reference: null,
slug: "l",
sortOrder: 3,
value: ""
@ -514,6 +550,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEx",
name: "XL",
reference: null,
slug: "xl",
sortOrder: 4,
value: ""
@ -523,6 +560,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
file: null,
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEy",
name: "XXL",
reference: null,
slug: "xxl",
sortOrder: 5,
value: ""

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
import { AttributeCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeCreate
@ -32,6 +32,7 @@ export interface AttributeCreate_attributeCreate_attribute_values {
name: string | null;
slug: string | null;
file: AttributeCreate_attributeCreate_attribute_values_file | null;
reference: string | null;
}
export interface AttributeCreate_attributeCreate_attribute {
@ -47,6 +48,7 @@ export interface AttributeCreate_attributeCreate_attribute {
privateMetadata: (AttributeCreate_attributeCreate_attribute_privateMetadata | null)[];
availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null;
entityType: AttributeEntityTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeCreate_attributeCreate_attribute_values | null)[] | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeTypeEnum, AttributeInputTypeEnum } from "./../../types/globalTypes";
import { AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum } from "./../../types/globalTypes";
// ====================================================
// GraphQL query operation: AttributeDetails
@ -32,6 +32,7 @@ export interface AttributeDetails_attribute_values {
name: string | null;
slug: string | null;
file: AttributeDetails_attribute_values_file | null;
reference: string | null;
}
export interface AttributeDetails_attribute {
@ -47,6 +48,7 @@ export interface AttributeDetails_attribute {
privateMetadata: (AttributeDetails_attribute_privateMetadata | null)[];
availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null;
entityType: AttributeEntityTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeDetails_attribute_values | null)[] | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeUpdateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
import { AttributeUpdateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeUpdate
@ -32,6 +32,7 @@ export interface AttributeUpdate_attributeUpdate_attribute_values {
name: string | null;
slug: string | null;
file: AttributeUpdate_attributeUpdate_attribute_values_file | null;
reference: string | null;
}
export interface AttributeUpdate_attributeUpdate_attribute {
@ -47,6 +48,7 @@ export interface AttributeUpdate_attributeUpdate_attribute {
privateMetadata: (AttributeUpdate_attributeUpdate_attribute_privateMetadata | null)[];
availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null;
entityType: AttributeEntityTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeUpdate_attributeUpdate_attribute_values | null)[] | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueCreate
@ -32,6 +32,7 @@ export interface AttributeValueCreate_attributeValueCreate_attribute_values {
name: string | null;
slug: string | null;
file: AttributeValueCreate_attributeValueCreate_attribute_values_file | null;
reference: string | null;
}
export interface AttributeValueCreate_attributeValueCreate_attribute {
@ -47,6 +48,7 @@ export interface AttributeValueCreate_attributeValueCreate_attribute {
privateMetadata: (AttributeValueCreate_attributeValueCreate_attribute_privateMetadata | null)[];
availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null;
entityType: AttributeEntityTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeValueCreate_attributeValueCreate_attribute_values | null)[] | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
import { AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueDelete
@ -32,6 +32,7 @@ export interface AttributeValueDelete_attributeValueDelete_attribute_values {
name: string | null;
slug: string | null;
file: AttributeValueDelete_attributeValueDelete_attribute_values_file | null;
reference: string | null;
}
export interface AttributeValueDelete_attributeValueDelete_attribute {
@ -47,6 +48,7 @@ export interface AttributeValueDelete_attributeValueDelete_attribute {
privateMetadata: (AttributeValueDelete_attributeValueDelete_attribute_privateMetadata | null)[];
availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null;
entityType: AttributeEntityTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeValueDelete_attributeValueDelete_attribute_values | null)[] | null;

View file

@ -2,7 +2,7 @@
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueUpdate
@ -32,6 +32,7 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute_values {
name: string | null;
slug: string | null;
file: AttributeValueUpdate_attributeValueUpdate_attribute_values_file | null;
reference: string | null;
}
export interface AttributeValueUpdate_attributeValueUpdate_attribute {
@ -47,6 +48,7 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute {
privateMetadata: (AttributeValueUpdate_attributeValueUpdate_attribute_privateMetadata | null)[];
availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null;
entityType: AttributeEntityTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeValueUpdate_attributeValueUpdate_attribute_values | null)[] | null;

View file

@ -1,17 +1,88 @@
import {
AttributeInput,
AttributeInputData
} from "@saleor/components/Attributes";
import { FileUpload } from "@saleor/files/types/FileUpload";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment";
import { UploadErrorFragment } from "@saleor/fragments/types/UploadErrorFragment";
import { FormsetData } from "@saleor/hooks/useFormset";
import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails";
import { ProductDetails_product_attributes } from "@saleor/products/types/ProductDetails";
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts";
import {
AttributeEntityTypeEnum,
AttributeInputTypeEnum,
AttributeValueInput
} from "@saleor/types/globalTypes";
import { mapNodeToChoice, mapPagesToChoices } from "@saleor/utils/maps";
import { MutationFetchResult } from "react-apollo";
import { AttributePageFormData } from "../components/AttributePage";
import { AttributeValueEditDialogFormData } from "../components/AttributeValueEditDialog";
import { AttributeValueDelete } from "../types/AttributeValueDelete";
export const ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES = [
AttributeInputTypeEnum.DROPDOWN,
AttributeInputTypeEnum.MULTISELECT
];
export interface AttributeReference {
label: string;
value: string;
}
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 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<null, File>,
existingAttribute:
@ -54,6 +125,18 @@ export const mergeAttributeValueDeleteErrors = (
return errors;
}, []);
export const mergeAttributeValues = (
attributeId: string,
attributeValues: string[],
attributes: FormsetData<AttributeInputData, string[]>
) => {
const attribute = attributes.find(attribute => attribute.id === attributeId);
return attribute.value
? [...attribute.value, ...attributeValues]
: attributeValues;
};
export const getFileValuesToUploadFromAttributes = (
attributesWithNewFileValue: FormsetData<null, File>
) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value);
@ -104,3 +187,131 @@ export const getAttributesAfterFileAttributesUpdate = (
return uploadedFileAttributes.concat(removedFileAttributes);
};
export const getFileAttributeDisplayData = (
attribute: AttributeInput,
attributesWithNewFileValue: FormsetData<null, File>
) => {
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<null, File>,
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 [];
};

View file

@ -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<AttributeInputData, string[]> = [
{
data: {
@ -15,6 +14,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
file: null,
id: "attrv-1",
name: "Attribute 1 Value 1",
reference: null,
slug: "attr-1-v-1"
}
]
@ -33,6 +33,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
file: null,
id: "attrv-2",
name: "Attribute 2 Value 1",
reference: null,
slug: "attr-2-v-1"
},
{
@ -40,6 +41,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
file: null,
id: "attrv-3",
name: "Attribute 2 Value 2",
reference: null,
slug: "attr-2-v-2"
},
{
@ -47,6 +49,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
file: null,
id: "attrv-4",
name: "Attribute 2 Value 3",
reference: null,
slug: "attr-2-v-3"
}
]
@ -69,6 +72,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
},
id: "gdghdgdhkkdae",
name: "File First Value",
reference: null,
slug: "file-first-value"
}
]

View file

@ -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<string[]>,
triggerChange: () => void
): FormsetChange<string> {
return (attributeId: string, value: string) => {
triggerChange();
changeAttributeData(attributeId, value === "" ? [] : [value]);
};
}
export function createAttributeMultiChangeHandler(
changeAttributeData: FormsetChange<string[]>,
attributes: FormsetData<AttributeInputData, string[]>,
triggerChange: () => void
): FormsetChange<string> {
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<string[]>,
triggerChange: () => void
): FormsetChange<string[]> {
return (attributeId: string, values: string[]) => {
changeAttributeData(attributeId, values);
triggerChange();
};
}
export function createFetchReferencesHandler(
attributes: FormsetData<AttributeInputData, string[]>,
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<AttributeInputData, string[]>,
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<string[]>,
attributesWithNewFileValue: FormsetData<FormsetData<null, File>>,
addAttributeNewFileValue: (data: FormsetAtomicData<null, File>) => void,
changeAttributeNewFileValue: FormsetChange<File>,
triggerChange: () => void
): FormsetChange<File> {
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<string[]>,
attributes: FormsetData<AttributeInputData, string[]>,
triggerChange: () => void
): FormsetChange<ReorderEvent> {
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

View file

@ -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<AttributeDetailsProps> = ({ params }) => {
const navigate = useNavigator();
const notify = useNotifier();
@ -145,10 +116,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ 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<AttributeDetailsProps> = ({ params }) => {
__typename: "AttributeValue" as "AttributeValue",
file: null,
id: valueIndex.toString(),
reference: null,
slug: slugify(value.name).toLowerCase(),
sortOrder: valueIndex,
value: null,

View file

@ -178,6 +178,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
const handleUpdate = async (data: AttributePageFormData) => {
const input = {
...data,
entityType: undefined,
inputType: undefined,
metadata: undefined,
privateMetadata: undefined,

View file

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

View file

@ -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<any>
) {
const [userContext, setUserContext] = useState<undefined | User>(undefined);
const autologinPromise = useRef<Promise<any>>();
const refreshPromise = useRef<Promise<boolean>>();
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<RefreshToken, RefreshTokenVariables>(
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<boolean> => {
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<AuthProviderProps> = ({ 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 (
<UserContext.Provider
value={{
login,
loginByToken,
logout,
tokenAuthLoading: tokenAuthOpts.loading,
tokenRefresh: refreshToken,
tokenVerifyLoading: tokenVerifyOpts.loading,
user: userContext
}}
>
{children}
</UserContext.Provider>
<UserContext.Provider value={authProvider}>{children}</UserContext.Provider>
);
};

View file

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

View file

@ -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<LoginCardProps, "classes"> = {
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", () => <LoginPage {...props} />)
.add("error", () => <LoginPage {...props} error={true} />)
.add("loading", () => <LoginPage {...props} disableLoginButton={true} />);
.add("disabled", () => <LoginPage {...props} disabled={true} />)
.add("loading", () => <LoginPage {...props} loading={true} />);

View file

@ -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<LoginCardProps> = 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 (
<div className={classes.loading}>
<CircularProgress size={128} />
</div>
);
}
return (
<Form initial={initialFormData} onSubmit={onSubmit}>
<LoginForm onSubmit={onSubmit}>
{({ change: handleChange, data, submit: handleSubmit }) => (
<>
{error && (
@ -73,6 +91,13 @@ const LoginCard: React.FC<LoginCardProps> = props => {
</Typography>
</div>
)}
{externalError && (
<div className={classes.panel} data-test="loginErrorMessage">
<Typography variant="caption">
<FormattedMessage defaultMessage="Sorry, login went wrong. Please try again." />
</Typography>
</div>
)}
<TextField
autoFocus
fullWidth
@ -84,6 +109,7 @@ const LoginCard: React.FC<LoginCardProps> = props => {
inputProps={{
"data-test": "email"
}}
disabled={disabled}
/>
<FormSpacer />
<TextField
@ -99,13 +125,14 @@ const LoginCard: React.FC<LoginCardProps> = props => {
inputProps={{
"data-test": "password"
}}
disabled={disabled}
/>
<FormSpacer />
<div className={classes.buttonContainer}>
<Button
className={classes.loginButton}
color="primary"
disabled={disableLoginButton}
disabled={disabled}
variant="contained"
onClick={handleSubmit}
type="submit"
@ -115,15 +142,56 @@ const LoginCard: React.FC<LoginCardProps> = props => {
</Button>
</div>
<FormSpacer />
<Typography className={classes.link} onClick={onPasswordRecovery}>
<Typography>
<FormattedMessage
defaultMessage="Reset your password"
description="button"
defaultMessage="Forgot password? {resetPasswordLink}"
description="description"
values={{
resetPasswordLink: (
<a className={classes.link} onClick={onPasswordRecovery}>
<FormattedMessage
defaultMessage="Use this link to recover it"
description="link"
/>
</a>
)
}}
/>
</Typography>
{externalAuthentications.length > 0 && (
<>
<FormSpacer />
<Divider />
<FormSpacer />
<Typography>
<FormattedMessage
defaultMessage="or login using"
description="description"
/>
</Typography>
</>
)}
</Form>
{externalAuthentications.map(externalAuthentication => (
<React.Fragment key={externalAuthentication.id}>
<FormSpacer />
<Button
color="primary"
fullWidth
variant="outlined"
size="large"
onClick={() =>
onExternalAuthentication(externalAuthentication.id)
}
data-test="external-authentication"
disabled={disabled}
>
{externalAuthentication.name}
</Button>
</React.Fragment>
))}
</>
)}
</LoginForm>
);
};
LoginCard.displayName = "LoginCard";

View file

@ -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<boolean>;
}
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<LoginFormProps> = ({ children, onSubmit }) => {
const props = useLoginForm(onSubmit);
return <form onSubmit={props.submit}>{children(props)}</form>;
};
LoginForm.displayName = "LoginForm";
export default LoginForm;

View file

@ -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<boolean>;
tokenVerifyLoading: boolean;
user?: User;
autologinPromise?: MutableRefObject<Promise<any>>;
}
export interface UseAuthProviderOpts {
intl: IntlShape;
notify: IMessageContext;
apolloClient: ApolloClient<any>;
}
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
};
}

View file

@ -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<void>;
loginByExternalPlugin: (
input: ExternalLoginInput
) => Promise<ExternalObtainAccessTokens_externalObtainAccessTokens>;
}
export interface UseExternalAuthProviderOpts extends UseAuthProviderOpts {
setAuthPlugin: SetLocalStorage<any>;
authPlugin: string;
}
const persistToken = false;
export function useExternalAuthProvider({
apolloClient,
authPlugin,
intl,
notify,
setAuthPlugin
}: UseExternalAuthProviderOpts): UseExternalAuthProvider {
const [userContext, setUserContext] = useState<undefined | User>(undefined);
const autologinPromise = useRef<Promise<any>>();
const refreshPromise = useRef<Promise<boolean>>();
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<boolean> => {
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
};
}

View file

@ -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<TokenAuth_tokenCreate>;
loginByToken: (auth: string, csrf: string, user: User) => void;
}
export interface UseSaleorAuthProviderOpts extends UseAuthProviderOpts {
setAuthPlugin: SetLocalStorage<any>;
authPlugin: string;
}
const persistToken = false;
export function useSaleorAuthProvider({
apolloClient,
authPlugin,
intl,
notify,
setAuthPlugin
}: UseSaleorAuthProviderOpts): UseSaleorAuthProvider {
const [userContext, setUserContext] = useState<undefined | User>(undefined);
const autologinPromise = useRef<Promise<any>>();
const refreshPromise = useRef<Promise<boolean>>();
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<RefreshToken, RefreshTokenVariables>(
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<boolean> => {
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
};
}

View file

@ -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<RouteComponentProps<any>> = () => {
const qs = parseQs(location.search.substr(1));
const params: LoginUrlQueryParams = qs;
return <LoginViewComponent params={params} />;
};
interface UserContext {
login: (username: string, password: string) => void;
login: (username: string, password: string) => Promise<TokenAuth_tokenCreate>;
loginByExternalPlugin: (
input: ExternalLoginInput
) => Promise<ExternalObtainAccessTokens_externalObtainAccessTokens>;
loginByToken: (auth: string, csrf: string, user: User) => void;
logout: () => void;
requestLoginByExternalPlugin: (
pluginId: string,
input: RequestExternalLoginInput
) => Promise<void>;
tokenAuthLoading: boolean;
tokenRefresh: () => Promise<boolean>;
tokenVerifyLoading: boolean;
user?: User;
autologinPromise?: MutableRefObject<Promise<any>>;
}
export const UserContext = React.createContext<UserContext>({
login: undefined,
loginByExternalPlugin: undefined,
loginByToken: undefined,
logout: undefined,
requestLoginByExternalPlugin: undefined,
tokenAuthLoading: false,
tokenRefresh: undefined,
tokenVerifyLoading: false

View file

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

12
src/auth/queries.ts Normal file
View file

@ -0,0 +1,12 @@
import gql from "graphql-tag";
export const availableExternalAuthentications = gql`
query AvailableExternalAuthentications {
shop {
availableExternalAuthentications {
id
name
}
}
}
`;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<LoginViewProps> = ({ 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>(
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 (
<LoginPage
error={user === null}
disableLoginButton={tokenAuthLoading}
error={isError}
externalError={isExternalError}
disabled={tokenAuthLoading}
externalAuthentications={
externalAuthentications?.shop?.availableExternalAuthentications
}
loading={externalAuthenticationsLoading || tokenAuthLoading}
onExternalAuthentication={handleRequestExternalAuthentication}
onPasswordRecovery={() => navigate(passwordResetUrl)}
onSubmit={handleSubmit}
/>

View file

@ -33,7 +33,7 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
}) => {
const intl = useIntl();
const formErrors = getFormErrors(["name", "descriptionJson"], errors);
const formErrors = getFormErrors(["name", "description"], errors);
return (
<Card>
@ -59,8 +59,8 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
<RichTextEditor
data={data.description}
disabled={disabled}
error={!!formErrors.descriptionJson}
helperText={getProductErrorMessage(formErrors.descriptionJson, intl)}
error={!!formErrors.description}
helperText={getProductErrorMessage(formErrors.description, intl)}
label={intl.formatMessage({
defaultMessage: "Category Description"
})}

View file

@ -56,7 +56,7 @@ function useCategoryUpdateForm(
slug: category?.slug || ""
});
const [description, changeDescription] = useRichText({
initial: category?.descriptionJson,
initial: category?.description,
triggerChange
});

View file

@ -105,7 +105,7 @@ export const category: (
startCursor: "YXJyYXljb25uZWN0aW9uOjA="
}
},
descriptionJson: JSON.stringify(content),
description: JSON.stringify(content),
id: "Q2F0ZWdvcnk6NA==",
metadata: [
{

View file

@ -39,7 +39,7 @@ export interface CategoryCreate_categoryCreate_category {
backgroundImage: CategoryCreate_categoryCreate_category_backgroundImage | null;
name: string;
slug: string;
descriptionJson: any;
description: any;
seoDescription: string | null;
seoTitle: string | null;
parent: CategoryCreate_categoryCreate_category_parent | null;

View file

@ -165,7 +165,7 @@ export interface CategoryDetails_category {
backgroundImage: CategoryDetails_category_backgroundImage | null;
name: string;
slug: string;
descriptionJson: any;
description: any;
seoDescription: string | null;
seoTitle: string | null;
parent: CategoryDetails_category_parent | null;

View file

@ -39,7 +39,7 @@ export interface CategoryUpdate_categoryUpdate_category {
backgroundImage: CategoryUpdate_categoryUpdate_category_backgroundImage | null;
name: string;
slug: string;
descriptionJson: any;
description: any;
seoDescription: string | null;
seoTitle: string | null;
parent: CategoryUpdate_categoryUpdate_category_parent | null;

View file

@ -48,7 +48,7 @@ export const CategoryCreateView: React.FC<CategoryCreateViewProps> = ({
const result = await createCategory({
variables: {
input: {
descriptionJson: JSON.stringify(formData.description),
description: JSON.stringify(formData.description),
name: formData.name,
seo: {
description: formData.seoDescription,

View file

@ -188,7 +188,7 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
id,
input: {
backgroundImageAlt: formData.backgroundImageAlt,
descriptionJson: JSON.stringify(formData.description),
description: JSON.stringify(formData.description),
name: formData.name,
seo: {
description: formData.seoDescription,

View file

@ -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<ChannelFormProps> = ({
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<ChannelFormProps> = ({
<CardContent>
{!!currencyCodes ? (
<SingleAutocompleteSelectField
data-test-id="channel-currency-select-input"
allowCustomValues
error={!!formErrors.currencyCode}
FormHelperTextProps={
{
"data-testid": "currency-text-input-helper-text"
} as ExtendedFormHelperTextProps
}
helperText={getChannelsErrorMessage(
formErrors?.currencyCode,
intl

View file

@ -0,0 +1,5 @@
import { FormHelperTextProps } from "@material-ui/core/FormHelperText";
export type ExtendedFormHelperTextProps = FormHelperTextProps & {
"data-testid": string;
};

View file

@ -1,10 +1,23 @@
import { channelDetailsFragment } from "@saleor/fragments/channels";
import {
channelDetailsFragment,
channelFragment
} from "@saleor/fragments/channels";
import makeQuery from "@saleor/hooks/makeQuery";
import gql from "graphql-tag";
import { BaseChannels } from "./types/BaseChannels";
import { Channel, ChannelVariables } from "./types/Channel";
import { Channels } from "./types/Channels";
export const channelsListBase = gql`
${channelFragment}
query BaseChannels {
channels {
...ChannelFragment
}
}
`;
export const channelsList = gql`
${channelDetailsFragment}
query Channels {
@ -23,6 +36,9 @@ export const channelDetails = gql`
}
`;
export const useBaseChannelsList = makeQuery<BaseChannels, {}>(
channelsListBase
);
export const useChannelsList = makeQuery<Channels, {}>(channelsList);
export const useChannelDetails = makeQuery<Channel, ChannelVariables>(
channelDetails

View file

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

View file

@ -33,7 +33,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
}) => {
const intl = useIntl();
const formErrors = getFormErrors(["name", "descriptionJson"], errors);
const formErrors = getFormErrors(["name", "description"], errors);
return (
<Card>
@ -57,8 +57,8 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
<FormSpacer />
<RichTextEditor
data={data.description}
error={!!formErrors.descriptionJson}
helperText={getProductErrorMessage(formErrors.descriptionJson, intl)}
error={!!formErrors.description}
helperText={getProductErrorMessage(formErrors.description, intl)}
label={intl.formatMessage(commonMessages.description)}
name="description"
disabled={disabled}

View file

@ -68,7 +68,7 @@ function useCollectionUpdateForm(
slug: collection?.slug || ""
});
const [description, changeDescription] = useRichText({
initial: collection?.descriptionJson,
initial: collection?.description,
triggerChange
});

View file

@ -157,7 +157,7 @@ export const collection: (
publicationDate: null
}
],
descriptionJson: JSON.stringify(content),
description: JSON.stringify(content),
id: "Q29sbGVjdGlvbjox",
metadata: [
{

View file

@ -102,7 +102,7 @@ export interface CollectionDetails_collection {
privateMetadata: (CollectionDetails_collection_privateMetadata | null)[];
backgroundImage: CollectionDetails_collection_backgroundImage | null;
slug: string;
descriptionJson: any;
description: any;
seoDescription: string | null;
seoTitle: string | null;
products: CollectionDetails_collection_products | null;

View file

@ -48,7 +48,7 @@ export interface CollectionUpdate_collectionUpdate_collection {
privateMetadata: (CollectionUpdate_collectionUpdate_collection_privateMetadata | null)[];
backgroundImage: CollectionUpdate_collectionUpdate_collection_backgroundImage | null;
slug: string;
descriptionJson: any;
description: any;
seoDescription: string | null;
seoTitle: string | null;
}

View file

@ -48,7 +48,7 @@ export interface CreateCollection_collectionCreate_collection {
privateMetadata: (CreateCollection_collectionCreate_collection_privateMetadata | null)[];
backgroundImage: CreateCollection_collectionCreate_collection_backgroundImage | null;
slug: string;
descriptionJson: any;
description: any;
seoDescription: string | null;
seoTitle: string | null;
}

View file

@ -102,7 +102,7 @@ export const CollectionCreate: React.FC<CollectionCreateProps> = ({
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,

View file

@ -198,7 +198,7 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
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,

View file

@ -61,7 +61,11 @@ const AppHeader: React.FC<AppHeaderProps> = props => {
{anchor =>
anchor ? (
<Portal container={anchor.current}>
<div className={classes.root} onClick={onBack}>
<div
className={classes.root}
onClick={onBack}
data-test-id="app-header-back-button"
>
<ArrowBackIcon className={classes.backArrow} />
{children ? (
<Typography className={classes.title}>{children}</Typography>

View file

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

View file

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

View file

@ -239,8 +239,11 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
</div>
<main className={classes.view}>
{appState.error
? appState.error === "unhandled" && (
<ErrorPage onBack={handleErrorBack} />
? appState.error.type === "unhandled" && (
<ErrorPage
id={appState.error.id}
onBack={handleErrorBack}
/>
)
: children}
</main>

View file

@ -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<AssignAttributeValueDialogProps> = ({
attributeValues,
...rest
}) => {
const intl = useIntl();
return (
<AssignContainerDialog
containers={attributeValues.map(value => ({
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;

View file

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

View file

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

View file

@ -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<AttributeInputData, string[]>;
export type AttributeFileInput = FormsetAtomicData<AttributeInputData, File[]>;
@ -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<string>;
onMultiChange: FormsetChange<string>;
onFileChange: FormsetChange<File>;
onReferencesRemove: FormsetChange<string[]>;
onReferencesAddClick: (attribute: AttributeInput) => void;
onReferencesReorder: FormsetChange<ReorderEvent>;
}
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<AttributesProps> = ({
title,
onChange,
onMultiChange,
onFileChange
onFileChange,
onReferencesRemove,
onReferencesAddClick,
onReferencesReorder
}) => {
const intl = useIntl();
const classes = useStyles({});
@ -255,24 +310,42 @@ const Attributes: React.FC<AttributesProps> = ({
return (
<React.Fragment key={attribute.id}>
{attributeIndex > 0 && <Hr />}
<Grid className={classes.attributeSection} variant="uniform">
<div
className={classes.attributeSectionLabel}
data-test="attribute-label"
>
<Typography>{attribute.label}</Typography>
</div>
<div data-test="attribute-value">
{attribute.data.inputType ===
AttributeInputTypeEnum.REFERENCE ? (
<ExtendedAttributeRow
label={attribute.label}
selectLabel={intl.formatMessage({
defaultMessage: "Assign references",
description: "button label"
})}
onSelect={() => onReferencesAddClick(attribute)}
disabled={disabled}
>
<SortableChipsField
values={getReferenceDisplayValue(attribute)}
onValueDelete={value =>
onReferencesRemove(
attribute.id,
attribute.value?.filter(id => id !== value)
)
}
onValueReorder={event =>
onReferencesReorder(attribute.id, event)
}
loading={loading}
error={!!error}
helperText={getErrorMessage(error, intl)}
/>
</ExtendedAttributeRow>
) : attribute.data.inputType ===
AttributeInputTypeEnum.FILE ? (
<BasicAttributeRow label={attribute.label}>
<FileUploadField
className={classes.fileField}
disabled={disabled}
loading={loading}
file={getFileChoice(attribute)}
onFileUpload={file =>
onFileChange(attribute.id, file)
}
onFileUpload={file => onFileChange(attribute.id, file)}
onFileDelete={() =>
onFileChange(attribute.id, undefined)
}
@ -282,8 +355,10 @@ const Attributes: React.FC<AttributesProps> = ({
name: `attribute:${attribute.label}`
}}
/>
</BasicAttributeRow>
) : attribute.data.inputType ===
AttributeInputTypeEnum.DROPDOWN ? (
<BasicAttributeRow label={attribute.label}>
<SingleAutocompleteSelectField
choices={getSingleChoices(attribute.data.values)}
disabled={disabled}
@ -305,16 +380,16 @@ const Attributes: React.FC<AttributesProps> = ({
}
allowCustomValues={!attribute.data.isRequired}
/>
</BasicAttributeRow>
) : (
<BasicAttributeRow label={attribute.label}>
<MultiAutocompleteSelectField
choices={getMultiChoices(attribute.data.values)}
displayValues={getMultiDisplayValue(attribute)}
disabled={disabled}
error={!!error}
helperText={getErrorMessage(error, intl)}
label={intl.formatMessage(
messages.multipleValueLable
)}
label={intl.formatMessage(messages.multipleValueLable)}
name={`attribute:${attribute.label}`}
value={attribute.value}
onChange={event =>
@ -322,9 +397,8 @@ const Attributes: React.FC<AttributesProps> = ({
}
allowCustomValues={!attribute.data.isRequired}
/>
</BasicAttributeRow>
)}
</div>
</Grid>
</React.Fragment>
);
})}

View file

@ -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<BasicAttributeRowProps> = props => {
const { label, children } = props;
const classes = useStyles(props);
return (
<Grid className={classes.attributeSection} variant="uniform">
<div
className={classes.attributeSectionLabel}
data-test="attribute-label"
>
<Typography>{label}</Typography>
</div>
<div data-test="attribute-value">{children}</div>
</Grid>
);
};
BasicAttributeRow.displayName = "BasicAttributeRow";
export default BasicAttributeRow;

View file

@ -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<ExtendedAttributeRowProps> = props => {
const { label, selectLabel, disabled, onSelect, children } = props;
const classes = useStyles(props);
return (
<>
<Grid className={classes.attributeSection} variant="uniform">
<div
className={classes.attributeSectionLabel}
data-test="attribute-label"
>
<Typography>{label}</Typography>
</div>
<div data-test="attribute-selector">
<Button
className={classes.attributeSectionButton}
disabled={disabled}
variant="text"
color="primary"
data-test="button-attribute-selector"
onClick={onSelect}
>
{selectLabel}
</Button>
</div>
</Grid>
<div data-test="attribute-value">{children}</div>
</>
);
};
ExtendedAttributeRow.displayName = "ExtendedAttributeRow";
export default ExtendedAttributeRow;

View file

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

View file

@ -17,6 +17,7 @@ const user: User = {
email: "email@example.com",
firstName: "User",
id: "123",
isStaff: true,
lastName: "User",
userPermissions: [
{

View file

@ -417,7 +417,11 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityProps> = props =
userPermissions={user?.userPermissions || []}
requiredPermissions={[PermissionEnum.MANAGE_CHANNELS]}
>
<Button color="primary" onClick={openModal}>
<Button
color="primary"
onClick={openModal}
data-test-id="channels-availiability-manage-button"
>
{intl.formatMessage({
defaultMessage: "Manage",
description: "section header button"

View file

@ -77,7 +77,10 @@ export const ChannelsAvailabilityContent: React.FC<ChannelsAvailabilityContentPr
<Typography className={classes.contentTitle}>
<FormattedMessage defaultMessage="Channels A to Z" />
</Typography>
<div className={classes.scrollArea}>
<div
className={classes.scrollArea}
data-test-id="manage-products-channels-availiability-list"
>
{filteredChannels?.length ? (
filteredChannels.map(option => (
<div key={option.id} className={classes.option}>

View file

@ -1,10 +1,9 @@
import Chip, { ChipProps } from "@saleor/components/Chip";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import CardDecorator from "../../CardDecorator";
import Decorator from "../../Decorator";
const props: ChipProps = {
label: "Lorem Ipsum"
};

View file

@ -7,6 +7,7 @@ import SVG from "react-inlinesvg";
import { FormattedMessage } from "react-intl";
export interface ErrorPageProps {
id?: string | null;
onBack: () => void;
}
@ -31,6 +32,9 @@ const useStyles = makeStyles(
margin: "0 auto",
width: 830
},
errorId: {
marginTop: theme.spacing(3)
},
innerContainer: {
[theme.breakpoints.down("sm")]: {
order: 1,
@ -58,7 +62,7 @@ const useStyles = makeStyles(
);
const ErrorPage: React.FC<ErrorPageProps> = props => {
const { onBack } = props;
const { onBack, id } = props;
const classes = useStyles(props);
@ -79,6 +83,11 @@ const ErrorPage: React.FC<ErrorPageProps> = props => {
<Typography>
<FormattedMessage defaultMessage="Don't worry, everything is gonna be fine" />
</Typography>
{!!id && (
<Typography variant="subtitle2" className={classes.errorId}>
Error ID: {id}
</Typography>
)}
</div>
<div>
<Button

View file

@ -3,7 +3,6 @@ import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import InputLabel from "@material-ui/core/InputLabel";
import classNames from "classnames";
import Undo from "editorjs-undo";
import React from "react";
import { RichTextEditorContentProps, tools } from "./RichTextEditorContent";
@ -34,6 +33,8 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
const [isFocused, setFocus] = React.useState(false);
const editor = React.useRef<EditorJS>();
const editorContainer = React.useRef<HTMLDivElement>();
const prevTogglePromise = React.useRef<Promise<boolean>>(); // used to await subsequent toggle invocations
React.useEffect(
() => {
if (data) {
@ -46,8 +47,10 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
onChange(savedData);
},
onReady: () => {
const undo = new Undo({ editor });
undo.initialize(data);
// FIXME: This throws an error and is not working
// const undo = new Undo({ editor });
// undo.initialize(data);
if (onReady) {
onReady();
}
@ -62,10 +65,20 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
// Rerender editor only if changed from undefined to defined state
[data === undefined]
);
React.useEffect(() => {
const toggle = async () => {
if (editor.current?.readOnly) {
editor.current.readOnly.toggle(disabled);
// readOnly.toggle() by itself does not enqueue the events and will result in a broken output if invocations overlap
// Remove this logic when this is fixed in EditorJS
if (prevTogglePromise.current instanceof Promise) {
await prevTogglePromise.current;
}
prevTogglePromise.current = editor.current.readOnly.toggle(disabled);
}
};
toggle();
}, [disabled]);
return (

View file

@ -1,6 +1,7 @@
import { InputProps } from "@material-ui/core/Input";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import { ExtendedFormHelperTextProps } from "@saleor/channels/components/ChannelForm/types";
import { FetchMoreProps } from "@saleor/types";
import classNames from "classnames";
import Downshift, { ControllerStateAndHelpers } from "downshift";
@ -42,6 +43,7 @@ export interface SingleAutocompleteSelectFieldProps
InputProps?: InputProps;
fetchChoices?: (value: string) => void;
onChange: (event: React.ChangeEvent<any>) => void;
FormHelperTextProps?: ExtendedFormHelperTextProps;
}
const DebounceAutocomplete: React.ComponentType<DebounceProps<
@ -69,6 +71,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
fetchChoices,
onChange,
onFetchMore,
FormHelperTextProps,
...rest
} = props;
const classes = useStyles(props);
@ -178,6 +181,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
error={error}
disabled={disabled}
helperText={helperText}
FormHelperTextProps={FormHelperTextProps}
label={label}
fullWidth={true}
/>

View file

@ -0,0 +1,29 @@
import SortableChip, {
SortableChipProps
} from "@saleor/components/SortableChip";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import { SortableContainer } from "react-sortable-hoc";
const Container = SortableContainer(props => props.children);
const props: SortableChipProps = {
index: 0,
label: "Lorem Ipsum"
};
storiesOf("Generics / Sortable chip", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => (
<Container>
<SortableChip {...props} />
</Container>
))
.add("with x", () => (
<Container>
<SortableChip {...props} onClose={() => undefined} />
</Container>
));

View file

@ -0,0 +1,68 @@
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import CloseIcon from "@material-ui/icons/Close";
import classNames from "classnames";
import React from "react";
import { SortableElement, SortableElementProps } from "react-sortable-hoc";
import SortableHandle from "./SortableHandle";
export interface SortableChipProps extends SortableElementProps {
className?: string;
label: React.ReactNode;
onClose?: () => void;
}
const useStyles = makeStyles(
theme => ({
closeIcon: {
cursor: "pointer",
fontSize: 16,
marginLeft: theme.spacing(),
verticalAlign: "middle"
},
content: {
alignItems: "center",
display: "flex"
},
root: {
border: `1px solid ${theme.palette.divider}`,
borderRadius: 18,
display: "inline-block",
marginRight: theme.spacing(2),
padding: "6px 12px"
},
sortableHandle: {
marginRight: theme.spacing(1)
}
}),
{ name: "SortableChip" }
);
const SortableChip = SortableElement<SortableChipProps>(props => {
const { className, label, onClose } = props;
const classes = useStyles(props);
return (
<div className={classNames(classes.root, className)}>
<div className={classes.content}>
<SortableHandle
className={classes.sortableHandle}
data-test="button-drag-handle"
/>
<Typography data-test="chip-label">{label}</Typography>
{onClose && (
<CloseIcon
className={classes.closeIcon}
onClick={onClose}
data-test="button-close"
/>
)}
</div>
</div>
);
});
SortableChip.displayName = "SortableChip";
export default SortableChip;

View file

@ -0,0 +1,29 @@
import { makeStyles } from "@material-ui/core/styles";
import Draggable from "@saleor/icons/Draggable";
import classNames from "classnames";
import React from "react";
import { SortableHandle as SortableHandleHoc } from "react-sortable-hoc";
const useStyles = makeStyles(
{
drag: {
cursor: "grab"
}
},
{ name: "SortableHandle" }
);
interface SortableHandle {
className?: string;
}
const SortableHandle = SortableHandleHoc(props => {
const { className, ...restProps } = props;
const classes = useStyles(props);
return (
<Draggable className={classNames(classes.drag, className)} {...restProps} />
);
});
export default SortableHandle;

View file

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

View file

@ -0,0 +1,34 @@
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react";
import React from "react";
import SortableChipsField, {
SortableChipsFieldProps
} from "./SortableChipsField";
const props: SortableChipsFieldProps = {
onValueDelete: () => undefined,
onValueReorder: () => undefined,
values: [
{ label: "Item 1", value: "item-1" },
{ label: "Item 2", value: "item-2" },
{ label: "Item 3", value: "item-3" },
{ label: "Item 4", value: "item-4" },
{ label: "Item 5", value: "item-5" },
{ label: "Item 6", value: "item-6" }
]
};
storiesOf("Generics / Sortable chips field", module)
.addDecorator(CardDecorator)
.addDecorator(Decorator)
.add("default", () => <SortableChipsField {...props} />)
.add("loading", () => <SortableChipsField {...props} loading={true} />)
.add("with error", () => (
<SortableChipsField
{...props}
error={true}
helperText="Something went wrong"
/>
));

View file

@ -0,0 +1,84 @@
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import { ReorderAction } from "@saleor/types";
import React from "react";
import { SortableContainerProps } from "react-sortable-hoc";
import Skeleton from "../Skeleton";
import DraggableChip from "../SortableChip";
import SortableContainer from "./SortableContainer";
const useStyles = makeStyles(
theme => ({
chip: {
background: "#fff",
color: theme.palette.primary.dark,
marginBottom: theme.spacing(1)
},
errorText: {
color: theme.palette.error.light
}
}),
{
name: "SortableChipsField"
}
);
export interface SortableChipsFieldValueType {
label: string;
value: string;
}
export interface SortableChipsFieldProps extends SortableContainerProps {
loading?: boolean;
values: SortableChipsFieldValueType[];
error?: boolean;
helperText?: string;
onValueDelete: (id: string) => void;
onValueReorder: ReorderAction;
}
const SortableChipsField: React.FC<SortableChipsFieldProps> = props => {
const {
loading,
values,
error,
helperText,
onValueDelete,
onValueReorder
} = props;
const classes = useStyles(props);
return (
<SortableContainer
axis="xy"
lockAxis="xy"
useDragHandle
onSortEnd={onValueReorder}
>
<div>
{loading ? (
<Skeleton />
) : (
values.map((value, valueIndex) => (
<DraggableChip
className={classes.chip}
key={valueIndex}
index={valueIndex}
label={value.label}
onClose={() => onValueDelete(value.value)}
/>
))
)}
{error && (
<Typography variant="caption" className={classes.errorText}>
{helperText}
</Typography>
)}
</div>
</SortableContainer>
);
};
SortableChipsField.displayName = "SortableChipsField";
export default SortableChipsField;

View file

@ -0,0 +1,5 @@
import { SortableContainer as SortableContainerHoc } from "react-sortable-hoc";
const SortableContainer = SortableContainerHoc(({ children }) => children);
export default SortableContainer;

View file

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

View file

@ -19,11 +19,14 @@ const useStyles = makeStyles(
};
return {
alertDot: {
"&:before": { backgroundColor: yellow[500], ...dot }
},
errorDot: {
"&:before": { backgroundColor: theme.palette.error.main, ...dot }
},
neutralDot: {
"&:before": { backgroundColor: yellow[500], ...dot }
"&:before": { backgroundColor: grey[300], ...dot }
},
root: {
display: "inline-block",
@ -35,9 +38,6 @@ const useStyles = makeStyles(
},
successDot: {
"&:before": { backgroundColor: theme.palette.primary.main, ...dot }
},
unspecifiedDot: {
"&:before": { backgroundColor: grey[500], ...dot }
}
};
},
@ -47,7 +47,7 @@ const useStyles = makeStyles(
interface StatusLabelProps {
className?: string;
label: string | React.ReactNode;
status: "success" | "neutral" | "unspecified" | "error" | string;
status: "success" | "alert" | "neutral" | "error" | string;
typographyProps?: TypographyProps;
}
@ -62,8 +62,8 @@ const StatusLabel: React.FC<StatusLabelProps> = props => {
[classes.root]: true,
[className]: true,
[classes.successDot]: status === "success",
[classes.alertDot]: status === "alert",
[classes.neutralDot]: status === "neutral",
[classes.unspecifiedDot]: status === "unspecified",
[classes.errorDot]: status === "error"
})}
>

Some files were not shown because too many files have changed in this diff Show more