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. [ ] Data-test are added for new elements.
1. [ ] Type definitions are up to date. 1. [ ] Type definitions are up to date.
1. [ ] Changes are mentioned in the changelog. 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 ### Test environment config
<!-- Do not remove this section. It is required to properly setup test instance. <!-- 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. --> 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/ API_URI: https://master.staging.saleor.cloud/graphql/
APP_MOUNT_URI: /dashboard/ APP_MOUNT_URI: /dashboard/
STATIC_URL: /dashboard/static/ 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Package - name: Package
@ -41,4 +47,3 @@ jobs:
aws s3 sync build/dashboard s3://${{ secrets.AWS_STAGING_DEPLOYMENT_BUCKET }}/saleor-master-staging/static/ 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 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*" aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_STAGING_CF_DIST_ID }} --paths "/dashboard*"

View file

@ -59,7 +59,7 @@ jobs:
- name: Run build - name: Run build
env: env:
# Use custom API_URI or the default one # 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: / APP_MOUNT_URI: /
STATIC_URL: / STATIC_URL: /
run: | run: |
@ -88,7 +88,7 @@ jobs:
aws s3 sync ./build/storybook s3://${{ secrets.AWS_TEST_DEPLOYMENT_BUCKET }}/${{ steps.set-domain.outputs.domain }}/storybook aws s3 sync ./build/storybook s3://${{ secrets.AWS_TEST_DEPLOYMENT_BUCKET }}/${{ steps.set-domain.outputs.domain }}/storybook
- name: Invalidate cache - 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 - name: Update deployment status
uses: bobheadxi/deployments@v0.4.2 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 - Add shipping methods to translation section - #864 by @marekchoinski
- New Miscellaneous and Product refunds - #870 by @orzechdev - New Miscellaneous and Product refunds - #870 by @orzechdev
- Add zip code exclusion - #877 by @dominik-zeglen - 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 - Update quantity column in Inventory part of Product Variant view - #904 by @dominik-zeglen
- Add file attributes - #884 by @orzechdev - Add file attributes - #884 by @orzechdev
- Add shipping delivery days - #914 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 # 2.11.1

View file

@ -61,7 +61,7 @@ $ npm i
### Configuration ### 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. - `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/`. 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 $ 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) #### Crafted with ❤️ by [Mirumee Software](https://mirumee.com)

View file

@ -3,6 +3,14 @@
"context": "dialog header", "context": "dialog header",
"string": "Cancel Order" "string": "Cancel Order"
}, },
"amount title": {
"context": "amount title",
"string": "Refunded amount"
},
"by preposition": {
"context": "by preposition",
"string": "by"
},
"configurationMenuAttributes": { "configurationMenuAttributes": {
"string": "Determine attributes used to create product types" "string": "Determine attributes used to create product types"
}, },
@ -42,6 +50,42 @@
"configurationPluginsPages": { "configurationPluginsPages": {
"string": "View and update your plugins and their settings." "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": { "homeActivityCardHeader": {
"context": "header", "context": "header",
"string": "Activity" "string": "Activity"
@ -336,6 +380,10 @@
"context": "unassign product from sale, button", "context": "unassign product from sale, button",
"string": "Unassign" "string": "Unassign"
}, },
"shipment refund title": {
"context": "shipment refund title",
"string": "Shipment was refunded"
},
"shippingZoneDetailsDialogsDeleteShippingMethod": { "shippingZoneDetailsDialogsDeleteShippingMethod": {
"context": "delete shipping method", "context": "delete shipping method",
"string": "Are you sure you want to delete {name}?" "string": "Are you sure you want to delete {name}?"
@ -733,37 +781,53 @@
"context": "dialog content", "context": "dialog content",
"string": "Are you sure you want to delete {attributeName}?" "string": "Are you sure you want to delete {attributeName}?"
}, },
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_1005562666": { "src_dot_attributes_dot_components_dot_AttributeDetails_dot_attributeLabel": {
"context": "attribute's editor component", "context": "attribute's label",
"string": "Catalog Input type for Store Owner" "string": "Default Label"
}, },
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_1336738461": { "src_dot_attributes_dot_components_dot_AttributeDetails_dot_attributeSlug": {
"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": {
"context": "attribute's slug short code label", "context": "attribute's slug short code label",
"string": "Attribute Code" "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", "context": "attribute slug input field helper text",
"string": "This is used internally. Make sure you dont use spaces" "string": "This is used internally. Make sure you dont use spaces"
}, },
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_691600601": { "src_dot_attributes_dot_components_dot_AttributeDetails_dot_dropdown": {
"context": "attribute's label", "context": "product attribute type",
"string": "Default Label" "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": { "src_dot_attributes_dot_components_dot_AttributeListPage_dot_2417065806": {
"context": "tab name", "context": "tab name",
@ -844,43 +908,46 @@
"context": "page title", "context": "page title",
"string": "Create New Attribute" "string": "Create New Attribute"
}, },
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_1318123158": { "src_dot_attributes_dot_components_dot_AttributeProperties_dot_availableInGrid": {
"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": {
"context": "add attribute as column in product list table", "context": "add attribute as column in product list table",
"string": "Add to Column Options" "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", "context": "use attribute in filtering",
"string": "Use 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." "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": { "src_dot_attributes_dot_components_dot_AttributeValueDeleteDialog_dot_1326420604": {
"context": "delete attribute value", "context": "delete attribute value",
"string": "Are you sure you want to delete \"{name}\" value?" "string": "Are you sure you want to delete \"{name}\" value?"
@ -949,12 +1016,23 @@
"src_dot_auth_dot_components_dot_LoginPage_dot_2237029987": { "src_dot_auth_dot_components_dot_LoginPage_dot_2237029987": {
"string": "Password" "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": { "src_dot_auth_dot_components_dot_LoginPage_dot_3476994590": {
"string": "Sorry, your username and/or password are incorrect. Please try again." "string": "Sorry, your username and/or password are incorrect. Please try again."
}, },
"src_dot_auth_dot_components_dot_LoginPage_dot_4028609483": { "src_dot_auth_dot_components_dot_LoginPage_dot_3762459576": {
"context": "button", "context": "description",
"string": "Reset your password" "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": { "src_dot_auth_dot_components_dot_NewPasswordPage_dot_1254879564": {
"string": "New Password" "string": "New Password"
@ -1502,6 +1580,18 @@
"src_dot_components_dot_AssignAttributeDialog_dot_902296540": { "src_dot_components_dot_AssignAttributeDialog_dot_902296540": {
"string": "Search Attributes" "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": { "src_dot_components_dot_AssignCategoryDialog_dot_3125506097": {
"context": "dialog header", "context": "dialog header",
"string": "Assign Category" "string": "Assign Category"
@ -1543,6 +1633,10 @@
"src_dot_components_dot_AttributeUnassignDialog_dot_2037985699": { "src_dot_components_dot_AttributeUnassignDialog_dot_2037985699": {
"string": "Are you sure you want to unassign {attributeName} from {itemTypeName}?" "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": { "src_dot_components_dot_Attributes_dot_attributesNumber": {
"context": "number of attributes", "context": "number of attributes",
"string": "{number} Attributes" "string": "{number} Attributes"
@ -3032,6 +3126,9 @@
"src_dot_hooks_dot_3382262667": { "src_dot_hooks_dot_3382262667": {
"string": "Variant {name} has been set as default." "string": "Variant {name} has been set as default."
}, },
"src_dot_insufficientPermissions": {
"string": "Insufficient permissions"
},
"src_dot_lastName": { "src_dot_lastName": {
"string": "Last Name" "string": "Last Name"
}, },
@ -3130,13 +3227,17 @@
"src_dot_orders_dot_components_dot_OrderCustomer_dot_4282475982": { "src_dot_orders_dot_components_dot_OrderCustomer_dot_4282475982": {
"string": "Billing Address" "string": "Billing Address"
}, },
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_1854613983": { "src_dot_orders_dot_components_dot_OrderDetailsPage_dot_cancelOrder": {
"context": "button", "context": "cancel button",
"string": "Cancel order" "string": "Cancel order"
}, },
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_3086420445": { "src_dot_orders_dot_components_dot_OrderDetailsPage_dot_confirmOrder": {
"context": "save button", "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": { "src_dot_orders_dot_components_dot_OrderDraftCancelDialog_dot_1961675716": {
"context": "dialog header", "context": "dialog header",
@ -3272,6 +3373,37 @@
"context": "product's sku", "context": "product's sku",
"string": "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": { "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." "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", "context": "dialog header",
"string": "Add Tracking Code" "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": { "src_dot_orders_dot_components_dot_OrderHistory_dot_1230178536": {
"context": "order history message", "context": "order history message",
"string": "Order address was updated" "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": { "src_dot_orders_dot_components_dot_OrderHistory_dot_1463685940": {
"context": "order history message", "context": "order history message",
"string": "Order was marked as paid" "string": "Order was marked as paid"
@ -3489,9 +3560,6 @@
"context": "order history message", "context": "order history message",
"string": "Payment failed" "string": "Payment failed"
}, },
"src_dot_orders_dot_components_dot_OrderHistory_dot_492197448": {
"string": "Products refunded"
},
"src_dot_orders_dot_components_dot_OrderHistory_dot_493321552": { "src_dot_orders_dot_components_dot_OrderHistory_dot_493321552": {
"context": "order history message", "context": "order history message",
"string": "Order cancel information was sent to customer" "string": "Order cancel information was sent to customer"
@ -3512,6 +3580,14 @@
"context": "order history message", "context": "order history message",
"string": "Order was cancelled" "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": { "src_dot_orders_dot_components_dot_OrderInvoiceEmailSendDialog_dot_1821123638": {
"string": "Are you sure you want to send this invoice: {invoiceNumber} to the customer?" "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": { "src_dot_orders_dot_components_dot_OrderProductAddDialog_dot_353369701": {
"string": "No products matching given query" "string": "No products matching given query"
}, },
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1580639738": { "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_1134347598": {
"context": "order refund amount", "context": "product price",
"string": "Proposed refund amount" "string": "Price"
}, },
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1705174606": { "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_1895667608": {
"context": "order refund amount", "context": "product name",
"string": "Max Refund" "string": "Product"
}, },
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1734445951": { "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_2796503714": {
"context": "order refund amount", "context": "ordered product quantity",
"string": "Refund total amount" "string": "Quantity"
}, },
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2045860028": { "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_693960049": {
"context": "order refund amount", "context": "ordered product sku",
"string": "Authorized Amount" "string": "SKU"
}, },
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2854815744": { "src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_878013594": {
"context": "order refund amount", "context": "order line total price",
"string": "Previously refunded" "string": "Total"
}, },
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2907874606": { "src_dot_orders_dot_components_dot_OrderRefundFulfilledProducts_dot_1097582574": {
"context": "order refund amount", "context": "section header returned",
"string": "Selected products value" "string": "Fulfillment returned"
},
"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_1134347598": { "src_dot_orders_dot_components_dot_OrderRefundFulfilledProducts_dot_1134347598": {
"context": "tabel column header", "context": "tabel column header",
@ -3804,6 +3838,90 @@
"context": "page header with order number", "context": "page header with order number",
"string": "Order #{orderNumber}" "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": { "src_dot_orders_dot_components_dot_OrderRefundUnfulfilledProducts_dot_1134347598": {
"context": "tabel column header", "context": "tabel column header",
"string": "Price" "string": "Price"
@ -3851,6 +3969,82 @@
"context": "refund type", "context": "refund type",
"string": "Refund Products" "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": { "src_dot_orders_dot_components_dot_OrderSettingsPage_dot_1149215359": {
"context": "header", "context": "header",
"string": "Order settings" "string": "Order settings"
@ -3874,34 +4068,10 @@
"context": "dialog header", "context": "dialog header",
"string": "Edit Shipping Method" "string": "Edit Shipping Method"
}, },
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_1134347598": { "src_dot_orders_dot_components_dot_OrderUnfulfilledProductsCard_dot_2095687440": {
"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": {
"context": "button", "context": "button",
"string": "Fulfill" "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": { "src_dot_orders_dot_views_dot_OrderDetails_dot_1039259580": {
"string": "Were generating the invoice you requested. Please wait a couple of moments" "string": "Were generating the invoice you requested. Please wait a couple of moments"
}, },
@ -3991,6 +4161,18 @@
"context": "order refunded success message", "context": "order refunded success message",
"string": "Refunded Items" "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": { "src_dot_pageTypes": {
"context": "page types section name", "context": "page types section name",
"string": "Page Types" "string": "Page Types"
@ -4237,6 +4419,10 @@
"context": "payment status", "context": "payment status",
"string": "Partially refunded" "string": "Partially refunded"
}, },
"src_dot_partiallyReturned": {
"context": "order status",
"string": "Partially returned"
},
"src_dot_permissionGroups": { "src_dot_permissionGroups": {
"context": "permission groups section name", "context": "permission groups section name",
"string": "Permission Groups" "string": "Permission Groups"
@ -5272,6 +5458,10 @@
"src_dot_requiredField": { "src_dot_requiredField": {
"string": "This field is required" "string": "This field is required"
}, },
"src_dot_returned": {
"context": "order status",
"string": "Returned"
},
"src_dot_sales": { "src_dot_sales": {
"context": "sales section name", "context": "sales section name",
"string": "Sales" "string": "Sales"
@ -6251,6 +6441,9 @@
"src_dot_translations_dot_components_dot_TranslationsVouchersPage_dot_2599922713": { "src_dot_translations_dot_components_dot_TranslationsVouchersPage_dot_2599922713": {
"string": "Voucher Name" "string": "Voucher Name"
}, },
"src_dot_unauthorizedDashboardAccess": {
"string": "Only staff users can access the dashboard"
},
"src_dot_unconfirmed": { "src_dot_unconfirmed": {
"context": "order status", "context": "order status",
"string": "Unconfirmed" "string": "Unconfirmed"
@ -6707,6 +6900,10 @@
"context": "table column header", "context": "table column header",
"string": "Quantity" "string": "Quantity"
}, },
"transaction reference subtitle": {
"context": "transaction reference subtitle",
"string": "Transaction reference"
},
"voucherDetailsUnassignCategory": { "voucherDetailsUnassignCategory": {
"context": "unassign category from voucher, button", "context": "unassign category from voucher, button",
"string": "Unassign" "string": "Unassign"

132
package-lock.json generated
View file

@ -30069,9 +30069,9 @@
} }
}, },
"@editorjs/editorjs": { "@editorjs/editorjs": {
"version": "2.19.0", "version": "2.19.1",
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.19.0.tgz", "resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.19.1.tgz",
"integrity": "sha512-8PUVaBZx69IrG8dNrE+FZbHSiRTR8ql8L/cmEi1mOdEdTqnOLq5Wv9dgemK00mBWEgNoavMAjtGQpItGknAa8A==", "integrity": "sha512-5lN7r5B2NCE8VJdsS3poX3Qg9rNwzpxZ+6Jjif3hAVZTYpQwg5wXEpAHFNbuavS0T5Ji+0ID31DQFotVI4PosA==",
"requires": { "requires": {
"codex-notifier": "^1.1.2", "codex-notifier": "^1.1.2",
"codex-tooltip": "^1.0.1" "codex-tooltip": "^1.0.1"
@ -31110,6 +31110,120 @@
"any-observable": "^0.3.0" "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": { "@sindresorhus/fnv1a": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-1.2.0.tgz",
@ -36245,9 +36359,9 @@
"integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==" "integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg=="
}, },
"codex-tooltip": { "codex-tooltip": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.1.tgz", "resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.2.tgz",
"integrity": "sha512-1xLb1NZbxguNtf02xBRhDphq/EXvMMeEbY0ievjQTHqf8UjXsD41evGk9rqcbjpl+JOjNgtwnp1OaU/X/h6fhQ==" "integrity": "sha512-oC+Bu5X/zyhbPydgMSLWKoM/+vkJMqaLWu3Dt/jZgXS3MWK23INwC5DMBrVXZSufAFk0i0SUni38k9rLMyZn/w=="
}, },
"collapse-white-space": { "collapse-white-space": {
"version": "1.0.6", "version": "1.0.6",
@ -46708,6 +46822,12 @@
"ipaddr.js": "1.9.0" "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": { "prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",

View file

@ -17,7 +17,7 @@
"npm": ">=6.11.0" "npm": ">=6.11.0"
}, },
"dependencies": { "dependencies": {
"@editorjs/editorjs": "^2.19.0", "@editorjs/editorjs": "^2.19.1",
"@editorjs/header": "^2.6.1", "@editorjs/header": "^2.6.1",
"@editorjs/image": "^2.6.0", "@editorjs/image": "^2.6.0",
"@editorjs/list": "^1.6.1", "@editorjs/list": "^1.6.1",
@ -27,6 +27,7 @@
"@material-ui/styles": "^4.5.2", "@material-ui/styles": "^4.5.2",
"@saleor/macaw-ui": "^0.1.1-9", "@saleor/macaw-ui": "^0.1.1-9",
"@types/faker": "^5.1.6", "@types/faker": "^5.1.6",
"@sentry/react": "^6.0.0",
"apollo": "^2.21.2", "apollo": "^2.21.2",
"apollo-cache-inmemory": "^1.6.5", "apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8", "apollo-client": "^2.6.8",
@ -94,6 +95,7 @@
"@pollyjs/adapter-node-http": "^5.0.0", "@pollyjs/adapter-node-http": "^5.0.0",
"@pollyjs/core": "^5.0.0", "@pollyjs/core": "^5.0.0",
"@pollyjs/persister-fs": "^5.0.0", "@pollyjs/persister-fs": "^5.0.0",
"@sentry/webpack-plugin": "^1.14.0",
"@storybook/addon-storyshots": "^5.2.8", "@storybook/addon-storyshots": "^5.2.8",
"@storybook/react": "^5.1.9", "@storybook/react": "^5.1.9",
"@testing-library/react-hooks": "^1.1.0", "@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:e2e:dev": "start-server-and-test start http://localhost:9000 cy:open",
"test": "jest src/", "test": "jest src/",
"transpile-messages": "node scripts/transpile-tx.js", "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": [ "entries": [
{ {
"_id": "f515e15cbc83df73e5bd41437971c2e6", "_id": "a3088678db2635ada66ab049f76c9722",
"_order": 0, "_order": 0,
"cache": {}, "cache": {},
"request": { "request": {
"bodySize": 691, "bodySize": 702,
"cookies": [], "cookies": [],
"headers": [ "headers": [
{ {
@ -28,7 +28,7 @@
{ {
"_fromType": "array", "_fromType": "array",
"name": "content-length", "name": "content-length",
"value": "691" "value": "702"
}, },
{ {
"_fromType": "array", "_fromType": "array",
@ -56,61 +56,53 @@
"postData": { "postData": {
"mimeType": "application/json", "mimeType": "application/json",
"params": [], "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": [], "queryString": [],
"url": "http://localhost:8000/graphql/" "url": "http://localhost:8000/graphql/"
}, },
"response": { "response": {
"bodySize": 1619, "bodySize": 1765,
"content": { "content": {
"mimeType": "application/json", "mimeType": "application/json",
"size": 1619, "size": 1765,
"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\"}}}]" "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": [], "cookies": [],
"headers": [ "headers": [
{ {
"name": "date", "name": "date",
"value": "Wed, 29 Jul 2020 13:10:18 GMT" "value": "Thu, 14 Jan 2021 14:10:40 GMT"
}, },
{ {
"name": "server", "name": "server",
"value": "WSGIServer/0.2 CPython/3.8.1" "value": "WSGIServer/0.2 CPython/3.8.7"
}, },
{ {
"name": "content-type", "name": "content-type",
"value": "application/json" "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", "name": "content-length",
"value": "1619" "value": "1765"
}, },
{ {
"name": "x-content-type-options", "name": "x-content-type-options",
"value": "nosniff" "value": "nosniff"
},
{
"name": "referrer-policy",
"value": "same-origin"
} }
], ],
"headersSize": 336, "headersSize": 194,
"httpVersion": "HTTP/1.1", "httpVersion": "HTTP/1.1",
"redirectURL": "", "redirectURL": "",
"status": 200, "status": 200,
"statusText": "OK" "statusText": "OK"
}, },
"startedDateTime": "2020-07-29T13:10:18.327Z", "startedDateTime": "2021-01-14T14:10:40.434Z",
"time": 23, "time": 155,
"timings": { "timings": {
"blocked": -1, "blocked": -1,
"connect": -1, "connect": -1,
@ -118,7 +110,7 @@
"receive": 0, "receive": 0,
"send": 0, "send": 0,
"ssl": -1, "ssl": -1,
"wait": 23 "wait": 155
} }
} }
], ],

View file

@ -8,11 +8,11 @@
}, },
"entries": [ "entries": [
{ {
"_id": "7c460842cac4a92c188d5451dfc533a2", "_id": "a2b8a02f624e52cd2b73a831f65d9a52",
"_order": 0, "_order": 0,
"cache": {}, "cache": {},
"request": { "request": {
"bodySize": 587, "bodySize": 598,
"cookies": [], "cookies": [],
"headers": [ "headers": [
{ {
@ -28,7 +28,7 @@
{ {
"_fromType": "array", "_fromType": "array",
"name": "content-length", "name": "content-length",
"value": "587" "value": "598"
}, },
{ {
"_fromType": "array", "_fromType": "array",
@ -56,74 +56,65 @@
"postData": { "postData": {
"mimeType": "application/json", "mimeType": "application/json",
"params": [], "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": [], "queryString": [],
"url": "http://localhost:8000/graphql/" "url": "http://localhost:8000/graphql/"
}, },
"response": { "response": {
"bodySize": 1830, "bodySize": 1976,
"content": { "content": {
"mimeType": "application/json", "mimeType": "application/json",
"size": 1830, "size": 1976,
"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\"}}}]" "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": [ "cookies": [
{ {
"httpOnly": true, "httpOnly": true,
"name": "refreshToken", "name": "refreshToken",
"path": "/", "path": "/",
"secure": true, "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMzIyNDU0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lOQT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6IlVJenpKU0ZhbFM4cHBsZk0xajVRTklVTmlYYjBWRkgza2JlNmtmVGRkWXZMako5RGhNYXNDdEhKS1hvR0RmYncifQ.Br0GWGPPcnysyUxukjBBfXNbwCAm2qlR5OYClwFF3ZQ"
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5ODYyMDIxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6InJMUE5NR05ZS1hIOFZZNFVORVdsNG5FT0ZNc2VvY2xqaW9pZ1BsMzZJTTJDcWJkbU9URXBOd3ZkSEJBSjFaV1EifQ.boD8G4pkSnZF-PLl5oOg85Uj-mqTiAzOkua9aAG3Bz4"
} }
], ],
"headers": [ "headers": [
{ {
"name": "date", "name": "date",
"value": "Wed, 29 Jul 2020 13:10:18 GMT" "value": "Thu, 14 Jan 2021 13:55:44 GMT"
}, },
{ {
"name": "server", "name": "server",
"value": "WSGIServer/0.2 CPython/3.8.1" "value": "WSGIServer/0.2 CPython/3.8.7"
}, },
{ {
"name": "content-type", "name": "content-type",
"value": "application/json" "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", "name": "content-length",
"value": "1830" "value": "1976"
}, },
{ {
"name": "x-content-type-options", "name": "x-content-type-options",
"value": "nosniff" "value": "nosniff"
}, },
{
"name": "referrer-policy",
"value": "same-origin"
},
{ {
"_fromType": "array", "_fromType": "array",
"name": "set-cookie", "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", "httpVersion": "HTTP/1.1",
"redirectURL": "", "redirectURL": "",
"status": 200, "status": 200,
"statusText": "OK" "statusText": "OK"
}, },
"startedDateTime": "2020-07-29T13:10:18.064Z", "startedDateTime": "2021-01-14T13:55:44.094Z",
"time": 118, "time": 392,
"timings": { "timings": {
"blocked": -1, "blocked": -1,
"connect": -1, "connect": -1,
@ -131,7 +122,7 @@
"receive": 0, "receive": 0,
"send": 0, "send": 0,
"ssl": -1, "ssl": -1,
"wait": 118 "wait": 392
} }
} }
], ],

View file

@ -8,11 +8,11 @@
}, },
"entries": [ "entries": [
{ {
"_id": "4836098613648775386c1e10728424dd", "_id": "b1557b45bbbf7aed1a4a53f5141ca324",
"_order": 0, "_order": 0,
"cache": {}, "cache": {},
"request": { "request": {
"bodySize": 428, "bodySize": 439,
"cookies": [], "cookies": [],
"headers": [ "headers": [
{ {
@ -28,7 +28,7 @@
{ {
"_fromType": "array", "_fromType": "array",
"name": "content-length", "name": "content-length",
"value": "428" "value": "439"
}, },
{ {
"_fromType": "array", "_fromType": "array",
@ -56,7 +56,7 @@
"postData": { "postData": {
"mimeType": "application/json", "mimeType": "application/json",
"params": [], "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": [], "queryString": [],
"url": "http://localhost:8000/graphql/" "url": "http://localhost:8000/graphql/"
@ -72,28 +72,16 @@
"headers": [ "headers": [
{ {
"name": "date", "name": "date",
"value": "Wed, 29 Jul 2020 13:10:18 GMT" "value": "Thu, 14 Jan 2021 14:10:40 GMT"
}, },
{ {
"name": "server", "name": "server",
"value": "WSGIServer/0.2 CPython/3.8.1" "value": "WSGIServer/0.2 CPython/3.8.7"
}, },
{ {
"name": "content-type", "name": "content-type",
"value": "application/json" "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", "name": "content-length",
"value": "89" "value": "89"
@ -101,16 +89,20 @@
{ {
"name": "x-content-type-options", "name": "x-content-type-options",
"value": "nosniff" "value": "nosniff"
},
{
"name": "referrer-policy",
"value": "same-origin"
} }
], ],
"headersSize": 334, "headersSize": 192,
"httpVersion": "HTTP/1.1", "httpVersion": "HTTP/1.1",
"redirectURL": "", "redirectURL": "",
"status": 200, "status": 200,
"statusText": "OK" "statusText": "OK"
}, },
"startedDateTime": "2020-07-29T13:10:18.368Z", "startedDateTime": "2021-01-14T14:10:40.611Z",
"time": 6, "time": 25,
"timings": { "timings": {
"blocked": -1, "blocked": -1,
"connect": -1, "connect": -1,
@ -118,7 +110,7 @@
"receive": 0, "receive": 0,
"send": 0, "send": 0,
"ssl": -1, "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": [ "entries": [
{ {
"_id": "86487093ff8b070d496fcdc566e01adf", "_id": "d94d7821dc951e48c410d691d7eccdef",
"_order": 0, "_order": 0,
"cache": {}, "cache": {},
"request": { "request": {
"bodySize": 603, "bodySize": 614,
"cookies": [], "cookies": [],
"headers": [ "headers": [
{ {
@ -28,7 +28,7 @@
{ {
"_fromType": "array", "_fromType": "array",
"name": "content-length", "name": "content-length",
"value": "603" "value": "614"
}, },
{ {
"_fromType": "array", "_fromType": "array",
@ -56,7 +56,7 @@
"postData": { "postData": {
"mimeType": "application/json", "mimeType": "application/json",
"params": [], "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": [], "queryString": [],
"url": "http://localhost:8000/graphql/" "url": "http://localhost:8000/graphql/"
@ -72,28 +72,16 @@
"headers": [ "headers": [
{ {
"name": "date", "name": "date",
"value": "Wed, 29 Jul 2020 13:10:18 GMT" "value": "Thu, 14 Jan 2021 13:55:45 GMT"
}, },
{ {
"name": "server", "name": "server",
"value": "WSGIServer/0.2 CPython/3.8.1" "value": "WSGIServer/0.2 CPython/3.8.7"
}, },
{ {
"name": "content-type", "name": "content-type",
"value": "application/json" "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", "name": "content-length",
"value": "214" "value": "214"
@ -101,16 +89,20 @@
{ {
"name": "x-content-type-options", "name": "x-content-type-options",
"value": "nosniff" "value": "nosniff"
},
{
"name": "referrer-policy",
"value": "same-origin"
} }
], ],
"headersSize": 335, "headersSize": 193,
"httpVersion": "HTTP/1.1", "httpVersion": "HTTP/1.1",
"redirectURL": "", "redirectURL": "",
"status": 200, "status": 200,
"statusText": "OK" "statusText": "OK"
}, },
"startedDateTime": "2020-07-29T13:10:18.208Z", "startedDateTime": "2021-01-14T13:55:44.521Z",
"time": 99, "time": 1183,
"timings": { "timings": {
"blocked": -1, "blocked": -1,
"connect": -1, "connect": -1,
@ -118,7 +110,7 @@
"receive": 0, "receive": 0,
"send": 0, "send": 0,
"ssl": -1, "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]! privateMetadata: [MetadataItem]!
metadata: [MetadataItem]! metadata: [MetadataItem]!
inputType: AttributeInputTypeEnum inputType: AttributeInputTypeEnum
entityType: AttributeEntityTypeEnum
name: String name: String
slug: String slug: String
type: AttributeTypeEnum type: AttributeTypeEnum
@ -431,6 +432,7 @@ type AttributeCreate {
input AttributeCreateInput { input AttributeCreateInput {
inputType: AttributeInputTypeEnum inputType: AttributeInputTypeEnum
entityType: AttributeEntityTypeEnum
name: String! name: String!
slug: String slug: String
type: AttributeTypeEnum! type: AttributeTypeEnum!
@ -450,6 +452,11 @@ type AttributeDelete {
attribute: Attribute attribute: Attribute
} }
enum AttributeEntityTypeEnum {
PAGE
PRODUCT
}
type AttributeError { type AttributeError {
field: String field: String
message: String message: String
@ -490,6 +497,7 @@ enum AttributeInputTypeEnum {
DROPDOWN DROPDOWN
MULTISELECT MULTISELECT
FILE FILE
REFERENCE
} }
type AttributeReorderValues { type AttributeReorderValues {
@ -563,9 +571,9 @@ type AttributeValue implements Node {
id: ID! id: ID!
name: String name: String
slug: 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 translation(languageCode: LanguageCodeEnum!): AttributeValueTranslation
inputType: AttributeInputTypeEnum inputType: AttributeInputTypeEnum
reference: ID
file: File file: File
} }
@ -598,6 +606,7 @@ input AttributeValueInput {
values: [String] values: [String]
file: String file: String
contentType: String contentType: String
references: [ID!]
} }
type AttributeValueTranslatableContent implements Node { type AttributeValueTranslatableContent implements Node {
@ -619,13 +628,6 @@ type AttributeValueTranslation implements Node {
language: LanguageDisplay! language: LanguageDisplay!
} }
enum AttributeValueType {
COLOR
GRADIENT
URL
STRING
}
type AttributeValueUpdate { type AttributeValueUpdate {
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
attribute: Attribute attribute: Attribute
@ -643,6 +645,7 @@ type BulkProductError {
message: String message: String
code: ProductErrorCode! code: ProductErrorCode!
attributes: [ID!] attributes: [ID!]
values: [ID!]
index: Int index: Int
warehouses: [ID!] warehouses: [ID!]
channels: [ID!] channels: [ID!]
@ -653,6 +656,7 @@ type BulkStockError {
message: String message: String
code: ProductErrorCode! code: ProductErrorCode!
attributes: [ID!] attributes: [ID!]
values: [ID!]
index: Int index: Int
} }
@ -667,13 +671,13 @@ type Category implements Node & ObjectWithMetadata {
seoDescription: String seoDescription: String
id: ID! id: ID!
name: String! name: String!
description: String! description: JSONString!
descriptionJson: JSONString!
slug: String! slug: String!
parent: Category parent: Category
level: Int! level: Int!
privateMetadata: [MetadataItem]! privateMetadata: [MetadataItem]!
metadata: [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 ancestors(before: String, after: String, first: Int, last: Int): CategoryCountableConnection
products(channel: String, before: String, after: String, first: Int, last: Int): ProductCountableConnection 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.") url: String @deprecated(reason: "This field will be removed after 2020-07-31.")
@ -717,8 +721,7 @@ input CategoryFilterInput {
} }
input CategoryInput { input CategoryInput {
description: String description: JSONString
descriptionJson: JSONString
name: String name: String
slug: String slug: String
seo: SeoInput seo: SeoInput
@ -743,8 +746,8 @@ type CategoryTranslatableContent implements Node {
seoDescription: String seoDescription: String
id: ID! id: ID!
name: String! name: String!
description: String! description: JSONString!
descriptionJson: JSONString! descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
translation(languageCode: LanguageCodeEnum!): CategoryTranslation translation(languageCode: LanguageCodeEnum!): CategoryTranslation
category: Category category: Category
} }
@ -760,9 +763,9 @@ type CategoryTranslation implements Node {
seoDescription: String seoDescription: String
id: ID! id: ID!
name: String! name: String!
description: String! description: JSONString!
descriptionJson: JSONString!
language: LanguageDisplay! language: LanguageDisplay!
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
} }
type CategoryUpdate { type CategoryUpdate {
@ -1047,11 +1050,11 @@ type Collection implements Node & ObjectWithMetadata {
seoDescription: String seoDescription: String
id: ID! id: ID!
name: String! name: String!
description: String! description: JSONString!
descriptionJson: JSONString!
slug: String! slug: String!
privateMetadata: [MetadataItem]! privateMetadata: [MetadataItem]!
metadata: [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 products(filter: ProductFilterInput, sortBy: ProductOrder, before: String, after: String, first: Int, last: Int): ProductCountableConnection
backgroundImage(size: Int): Image backgroundImage(size: Int): Image
translation(languageCode: LanguageCodeEnum!): CollectionTranslation translation(languageCode: LanguageCodeEnum!): CollectionTranslation
@ -1082,6 +1085,7 @@ type CollectionChannelListingError {
message: String message: String
code: ProductErrorCode! code: ProductErrorCode!
attributes: [ID!] attributes: [ID!]
values: [ID!]
channels: [ID!] channels: [ID!]
} }
@ -1117,8 +1121,7 @@ input CollectionCreateInput {
isPublished: Boolean isPublished: Boolean
name: String name: String
slug: String slug: String
description: String description: JSONString
descriptionJson: JSONString
backgroundImage: Upload backgroundImage: Upload
backgroundImageAlt: String backgroundImageAlt: String
seo: SeoInput seo: SeoInput
@ -1160,8 +1163,7 @@ input CollectionInput {
isPublished: Boolean isPublished: Boolean
name: String name: String
slug: String slug: String
description: String description: JSONString
descriptionJson: JSONString
backgroundImage: Upload backgroundImage: Upload
backgroundImageAlt: String backgroundImageAlt: String
seo: SeoInput seo: SeoInput
@ -1203,8 +1205,8 @@ type CollectionTranslatableContent implements Node {
seoDescription: String seoDescription: String
id: ID! id: ID!
name: String! name: String!
description: String! description: JSONString!
descriptionJson: JSONString! descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
translation(languageCode: LanguageCodeEnum!): CollectionTranslation translation(languageCode: LanguageCodeEnum!): CollectionTranslation
collection: Collection collection: Collection
} }
@ -1220,9 +1222,9 @@ type CollectionTranslation implements Node {
seoDescription: String seoDescription: String
id: ID! id: ID!
name: String! name: String!
description: String! description: JSONString!
descriptionJson: JSONString!
language: LanguageDisplay! language: LanguageDisplay!
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
} }
type CollectionUpdate { type CollectionUpdate {
@ -1941,6 +1943,49 @@ enum ExportScope {
FILTER 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 { type File {
url: String! url: String!
contentType: String contentType: String
@ -1994,9 +2039,21 @@ type FulfillmentRefundProducts {
orderErrors: [OrderError!]! 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 { enum FulfillmentStatus {
FULFILLED FULFILLED
REFUNDED REFUNDED
RETURNED
REPLACED
REFUNDED_AND_RETURNED
CANCELED CANCELED
} }
@ -2295,10 +2352,12 @@ type Margin {
stop: Int stop: Int
} }
type Menu implements Node { type Menu implements Node & ObjectWithMetadata {
id: ID! id: ID!
name: String! name: String!
slug: String! slug: String!
privateMetadata: [MetadataItem]!
metadata: [MetadataItem]!
items: [MenuItem] items: [MenuItem]
} }
@ -2365,7 +2424,7 @@ input MenuInput {
slug: String slug: String
} }
type MenuItem implements Node { type MenuItem implements Node & ObjectWithMetadata {
id: ID! id: ID!
name: String! name: String!
menu: Menu! menu: Menu!
@ -2374,6 +2433,8 @@ type MenuItem implements Node {
collection: Collection collection: Collection
page: Page page: Page
level: Int! level: Int!
privateMetadata: [MetadataItem]!
metadata: [MetadataItem]!
children: [MenuItem] children: [MenuItem]
url: String url: String
translation(languageCode: LanguageCodeEnum!): MenuItemTranslation translation(languageCode: LanguageCodeEnum!): MenuItemTranslation
@ -2596,6 +2657,7 @@ type Mutation {
productTypeBulkDelete(ids: [ID]!): ProductTypeBulkDelete productTypeBulkDelete(ids: [ID]!): ProductTypeBulkDelete
productTypeUpdate(id: ID!, input: ProductTypeInput!): ProductTypeUpdate productTypeUpdate(id: ID!, input: ProductTypeInput!): ProductTypeUpdate
productTypeReorderAttributes(moves: [ReorderInput]!, productTypeId: ID!, type: ProductAttributeType!): ProductTypeReorderAttributes productTypeReorderAttributes(moves: [ReorderInput]!, productTypeId: ID!, type: ProductAttributeType!): ProductTypeReorderAttributes
productReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, productId: ID!): ProductReorderAttributeValues
digitalContentCreate(input: DigitalContentUploadInput!, variantId: ID!): DigitalContentCreate digitalContentCreate(input: DigitalContentUploadInput!, variantId: ID!): DigitalContentCreate
digitalContentDelete(variantId: ID!): DigitalContentDelete digitalContentDelete(variantId: ID!): DigitalContentDelete
digitalContentUpdate(input: DigitalContentInput!, variantId: ID!): DigitalContentUpdate digitalContentUpdate(input: DigitalContentInput!, variantId: ID!): DigitalContentUpdate
@ -2611,6 +2673,7 @@ type Mutation {
productVariantSetDefault(productId: ID!, variantId: ID!): ProductVariantSetDefault productVariantSetDefault(productId: ID!, variantId: ID!): ProductVariantSetDefault
productVariantTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): ProductVariantTranslate productVariantTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): ProductVariantTranslate
productVariantChannelListingUpdate(id: ID!, input: [ProductVariantChannelListingAddInput!]!): ProductVariantChannelListingUpdate productVariantChannelListingUpdate(id: ID!, input: [ProductVariantChannelListingAddInput!]!): ProductVariantChannelListingUpdate
productVariantReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, variantId: ID!): ProductVariantReorderAttributeValues
variantImageAssign(imageId: ID!, variantId: ID!): VariantImageAssign variantImageAssign(imageId: ID!, variantId: ID!): VariantImageAssign
variantImageUnassign(imageId: ID!, variantId: ID!): VariantImageUnassign variantImageUnassign(imageId: ID!, variantId: ID!): VariantImageUnassign
paymentCapture(amount: PositiveDecimal, paymentId: ID!): PaymentCapture paymentCapture(amount: PositiveDecimal, paymentId: ID!): PaymentCapture
@ -2630,6 +2693,7 @@ type Mutation {
pageAttributeAssign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeAssign pageAttributeAssign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeAssign
pageAttributeUnassign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeUnassign pageAttributeUnassign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeUnassign
pageTypeReorderAttributes(moves: [ReorderInput!]!, pageTypeId: ID!): PageTypeReorderAttributes pageTypeReorderAttributes(moves: [ReorderInput!]!, pageTypeId: ID!): PageTypeReorderAttributes
pageReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, pageId: ID!): PageReorderAttributeValues
draftOrderComplete(id: ID!): DraftOrderComplete draftOrderComplete(id: ID!): DraftOrderComplete
draftOrderCreate(input: DraftOrderCreateInput!): DraftOrderCreate draftOrderCreate(input: DraftOrderCreateInput!): DraftOrderCreate
draftOrderDelete(id: ID!): DraftOrderDelete draftOrderDelete(id: ID!): DraftOrderDelete
@ -2647,6 +2711,7 @@ type Mutation {
orderFulfillmentCancel(id: ID!, input: FulfillmentCancelInput!): FulfillmentCancel orderFulfillmentCancel(id: ID!, input: FulfillmentCancelInput!): FulfillmentCancel
orderFulfillmentUpdateTracking(id: ID!, input: FulfillmentUpdateTrackingInput!): FulfillmentUpdateTracking orderFulfillmentUpdateTracking(id: ID!, input: FulfillmentUpdateTrackingInput!): FulfillmentUpdateTracking
orderFulfillmentRefundProducts(input: OrderRefundProductsInput!, order: ID!): FulfillmentRefundProducts orderFulfillmentRefundProducts(input: OrderRefundProductsInput!, order: ID!): FulfillmentRefundProducts
orderFulfillmentReturnProducts(input: OrderReturnProductsInput!, order: ID!): FulfillmentReturnProducts
orderMarkAsPaid(id: ID!, transactionReference: String): OrderMarkAsPaid orderMarkAsPaid(id: ID!, transactionReference: String): OrderMarkAsPaid
orderRefund(amount: PositiveDecimal!, id: ID!): OrderRefund orderRefund(amount: PositiveDecimal!, id: ID!): OrderRefund
orderUpdate(id: ID!, input: OrderUpdateInput!): OrderUpdate orderUpdate(id: ID!, input: OrderUpdateInput!): OrderUpdate
@ -2743,6 +2808,11 @@ type Mutation {
tokenRefresh(csrfToken: String, refreshToken: String): RefreshToken tokenRefresh(csrfToken: String, refreshToken: String): RefreshToken
tokenVerify(token: String!): VerifyToken tokenVerify(token: String!): VerifyToken
tokensDeactivateAll: DeactivateAllUserTokens 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 requestPasswordReset(email: String!, redirectUrl: String!): RequestPasswordReset
confirmAccount(email: String!, token: String!): ConfirmAccount confirmAccount(email: String!, token: String!): ConfirmAccount
setPassword(email: String!, password: String!, token: String!): SetPassword setPassword(email: String!, password: String!, token: String!): SetPassword
@ -2812,7 +2882,8 @@ type Order implements Node & ObjectWithMetadata {
shippingMethod: ShippingMethod shippingMethod: ShippingMethod
shippingMethodName: String shippingMethodName: String
channel: Channel! channel: Channel!
shippingPrice: TaxedMoney shippingPrice: TaxedMoney!
shippingTaxRate: Float!
token: String! token: String!
voucher: Voucher voucher: Voucher
giftCards: [GiftCard] giftCards: [GiftCard]
@ -2831,16 +2902,16 @@ type Order implements Node & ObjectWithMetadata {
availableShippingMethods: [ShippingMethod] availableShippingMethods: [ShippingMethod]
invoices: [Invoice] invoices: [Invoice]
number: String number: String
isPaid: Boolean isPaid: Boolean!
paymentStatus: PaymentChargeStatusEnum paymentStatus: PaymentChargeStatusEnum!
paymentStatusDisplay: String paymentStatusDisplay: String!
payments: [Payment] payments: [Payment]
total: TaxedMoney total: TaxedMoney!
subtotal: TaxedMoney subtotal: TaxedMoney!
statusDisplay: String statusDisplay: String
canFinalize: Boolean! canFinalize: Boolean!
totalAuthorized: Money totalAuthorized: Money!
totalCaptured: Money totalCaptured: Money!
events: [OrderEvent] events: [OrderEvent]
totalBalance: Money! totalBalance: Money!
userEmail: String userEmail: String
@ -2909,6 +2980,7 @@ input OrderDraftFilterInput {
customer: String customer: String
created: DateRangeInput created: DateRangeInput
search: String search: String
channels: [ID]
} }
type OrderError { type OrderError {
@ -2944,8 +3016,7 @@ enum OrderErrorCode {
UNIQUE UNIQUE
VOID_INACTIVE_PAYMENT VOID_INACTIVE_PAYMENT
ZERO_QUANTITY ZERO_QUANTITY
INVALID_REFUND_QUANTITY INVALID_QUANTITY
CANNOT_REFUND_FULFILLMENT_LINE
INSUFFICIENT_STOCK INSUFFICIENT_STOCK
DUPLICATED_INPUT_ITEM DUPLICATED_INPUT_ITEM
NOT_AVAILABLE_IN_CHANNEL NOT_AVAILABLE_IN_CHANNEL
@ -2973,6 +3044,7 @@ type OrderEvent implements Node {
warehouse: Warehouse warehouse: Warehouse
transactionReference: String transactionReference: String
shippingCostsIncluded: Boolean shippingCostsIncluded: Boolean
relatedOrder: Order
} }
type OrderEventCountableConnection { type OrderEventCountableConnection {
@ -3006,6 +3078,7 @@ enum OrderEventsEmailsEnum {
enum OrderEventsEnum { enum OrderEventsEnum {
DRAFT_CREATED DRAFT_CREATED
DRAFT_CREATED_FROM_REPLACE
DRAFT_ADDED_PRODUCTS DRAFT_ADDED_PRODUCTS
DRAFT_REMOVED_PRODUCTS DRAFT_REMOVED_PRODUCTS
PLACED PLACED
@ -3014,6 +3087,7 @@ enum OrderEventsEnum {
CANCELED CANCELED
ORDER_MARKED_AS_PAID ORDER_MARKED_AS_PAID
ORDER_FULLY_PAID ORDER_FULLY_PAID
ORDER_REPLACEMENT_CREATED
UPDATED_ADDRESS UPDATED_ADDRESS
EMAIL_SENT EMAIL_SENT
CONFIRMED CONFIRMED
@ -3031,6 +3105,8 @@ enum OrderEventsEnum {
FULFILLMENT_RESTOCKED_ITEMS FULFILLMENT_RESTOCKED_ITEMS
FULFILLMENT_FULFILLED_ITEMS FULFILLMENT_FULFILLED_ITEMS
FULFILLMENT_REFUNDED FULFILLMENT_REFUNDED
FULFILLMENT_RETURNED
FULFILLMENT_REPLACED
TRACKING_UPDATED TRACKING_UPDATED
NOTE_ADDED NOTE_ADDED
OTHER OTHER
@ -3078,8 +3154,8 @@ type OrderLine implements Node {
taxRate: Float! taxRate: Float!
digitalContentUrl: DigitalContentUrl digitalContentUrl: DigitalContentUrl
thumbnail(size: Int): Image thumbnail(size: Int): Image
unitPrice: TaxedMoney unitPrice: TaxedMoney!
totalPrice: TaxedMoney totalPrice: TaxedMoney!
variant: ProductVariant variant: ProductVariant
translatedProductName: String! translatedProductName: String!
translatedVariantName: String! translatedVariantName: String!
@ -3124,6 +3200,26 @@ input OrderRefundProductsInput {
includeShippingCosts: Boolean = false 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 { type OrderSettings {
automaticallyConfirmAllNewOrders: Boolean! automaticallyConfirmAllNewOrders: Boolean!
} }
@ -3166,6 +3262,8 @@ enum OrderStatus {
UNCONFIRMED UNCONFIRMED
UNFULFILLED UNFULFILLED
PARTIALLY_FULFILLED PARTIALLY_FULFILLED
PARTIALLY_RETURNED
RETURNED
FULFILLED FULFILLED
CANCELED CANCELED
} }
@ -3213,8 +3311,7 @@ type Page implements Node & ObjectWithMetadata {
seoDescription: String seoDescription: String
id: ID! id: ID!
title: String! title: String!
content: String! content: JSONString!
contentJson: JSONString!
publicationDate: Date publicationDate: Date
isPublished: Boolean! isPublished: Boolean!
slug: String! slug: String!
@ -3222,6 +3319,7 @@ type Page implements Node & ObjectWithMetadata {
created: DateTime! created: DateTime!
privateMetadata: [MetadataItem]! privateMetadata: [MetadataItem]!
metadata: [MetadataItem]! metadata: [MetadataItem]!
contentJson: String! @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.")
translation(languageCode: LanguageCodeEnum!): PageTranslation translation(languageCode: LanguageCodeEnum!): PageTranslation
attributes: [SelectedAttribute!]! attributes: [SelectedAttribute!]!
} }
@ -3270,8 +3368,7 @@ type PageCreate {
input PageCreateInput { input PageCreateInput {
slug: String slug: String
title: String title: String
content: String content: JSONString
contentJson: JSONString
attributes: [AttributeValueInput!] attributes: [AttributeValueInput!]
isPublished: Boolean isPublished: Boolean
publicationDate: String publicationDate: String
@ -3290,6 +3387,7 @@ type PageError {
message: String message: String
code: PageErrorCode! code: PageErrorCode!
attributes: [ID!] attributes: [ID!]
values: [ID!]
} }
enum PageErrorCode { enum PageErrorCode {
@ -3316,14 +3414,19 @@ type PageInfo {
input PageInput { input PageInput {
slug: String slug: String
title: String title: String
content: String content: JSONString
contentJson: JSONString
attributes: [AttributeValueInput!] attributes: [AttributeValueInput!]
isPublished: Boolean isPublished: Boolean
publicationDate: String publicationDate: String
seo: SeoInput 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 { enum PageSortField {
TITLE TITLE
SLUG SLUG
@ -3342,8 +3445,8 @@ type PageTranslatableContent implements Node {
seoDescription: String seoDescription: String
id: ID! id: ID!
title: String! title: String!
content: String! content: JSONString!
contentJson: JSONString! contentJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.")
translation(languageCode: LanguageCodeEnum!): PageTranslation translation(languageCode: LanguageCodeEnum!): PageTranslation
page: Page page: Page
} }
@ -3359,17 +3462,16 @@ type PageTranslation implements Node {
seoDescription: String seoDescription: String
id: ID! id: ID!
title: String! title: String!
content: String! content: JSONString!
contentJson: JSONString!
language: LanguageDisplay! language: LanguageDisplay!
contentJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.")
} }
input PageTranslationInput { input PageTranslationInput {
seoTitle: String seoTitle: String
seoDescription: String seoDescription: String
title: String title: String
content: String content: JSONString
contentJson: JSONString
} }
type PageType implements Node & ObjectWithMetadata { type PageType implements Node & ObjectWithMetadata {
@ -3737,8 +3839,7 @@ type Product implements Node & ObjectWithMetadata {
seoTitle: String seoTitle: String
seoDescription: String seoDescription: String
name: String! name: String!
description: String! description: JSONString!
descriptionJson: JSONString!
productType: ProductType! productType: ProductType!
slug: String! slug: String!
category: Category category: Category
@ -3749,6 +3850,7 @@ type Product implements Node & ObjectWithMetadata {
rating: Float rating: Float
privateMetadata: [MetadataItem]! privateMetadata: [MetadataItem]!
metadata: [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.") url: String! @deprecated(reason: "This field will be removed after 2020-07-31.")
thumbnail(size: Int): Image thumbnail(size: Int): Image
pricing: ProductPricingInfo pricing: ProductPricingInfo
@ -3821,6 +3923,7 @@ type ProductChannelListingError {
message: String message: String
code: ProductErrorCode! code: ProductErrorCode!
attributes: [ID!] attributes: [ID!]
values: [ID!]
channels: [ID!] channels: [ID!]
} }
@ -3857,8 +3960,7 @@ input ProductCreateInput {
category: ID category: ID
chargeTaxes: Boolean chargeTaxes: Boolean
collections: [ID] collections: [ID]
description: String description: JSONString
descriptionJson: JSONString
name: String name: String
slug: String slug: String
taxCode: String taxCode: String
@ -3879,6 +3981,7 @@ type ProductError {
message: String message: String
code: ProductErrorCode! code: ProductErrorCode!
attributes: [ID!] attributes: [ID!]
values: [ID!]
} }
enum ProductErrorCode { enum ProductErrorCode {
@ -3988,8 +4091,7 @@ input ProductInput {
category: ID category: ID
chargeTaxes: Boolean chargeTaxes: Boolean
collections: [ID] collections: [ID]
description: String description: JSONString
descriptionJson: JSONString
name: String name: String
slug: String slug: String
taxCode: String taxCode: String
@ -4026,6 +4128,12 @@ type ProductPricingInfo {
priceRangeLocalCurrency: TaxedMoneyRange 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 { input ProductStockFilterInput {
warehouseIds: [ID!] warehouseIds: [ID!]
quantity: IntRangeInput quantity: IntRangeInput
@ -4036,8 +4144,8 @@ type ProductTranslatableContent implements Node {
seoTitle: String seoTitle: String
seoDescription: String seoDescription: String
name: String! name: String!
description: String! description: JSONString!
descriptionJson: JSONString! descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
translation(languageCode: LanguageCodeEnum!): ProductTranslation translation(languageCode: LanguageCodeEnum!): ProductTranslation
product: Product product: Product
} }
@ -4053,9 +4161,9 @@ type ProductTranslation implements Node {
seoTitle: String seoTitle: String
seoDescription: String seoDescription: String
name: String! name: String!
description: String! description: JSONString!
descriptionJson: JSONString!
language: LanguageDisplay! language: LanguageDisplay!
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
} }
type ProductType implements Node & ObjectWithMetadata { type ProductType implements Node & ObjectWithMetadata {
@ -4278,6 +4386,12 @@ type ProductVariantReorder {
productErrors: [ProductError!]! 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 { type ProductVariantSetDefault {
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
product: Product product: Product
@ -4405,7 +4519,7 @@ type Query {
permissionGroup(id: ID!): Group permissionGroup(id: ID!): Group
me: User me: User
staffUsers(filter: StaffUserInput, sortBy: UserSortingInput, before: String, after: String, first: Int, last: Int): UserCountableConnection 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] _entities(representations: [_Any]): [_Entity]
_service: _Service _service: _Service
} }
@ -4840,6 +4954,7 @@ input ShippingZoneUpdateInput {
type Shop { type Shop {
availablePaymentGateways(currency: String): [PaymentGateway!]! availablePaymentGateways(currency: String): [PaymentGateway!]!
availableExternalAuthentications: [ExternalAuthentication!]!
availableShippingMethods(channel: String!, address: AddressInput): [ShippingMethod] availableShippingMethods(channel: String!, address: AddressInput): [ShippingMethod]
geolocalization: Geolocalization geolocalization: Geolocalization
countries(languageCode: LanguageCodeEnum): [CountryDisplay!]! countries(languageCode: LanguageCodeEnum): [CountryDisplay!]!
@ -5215,8 +5330,7 @@ input TranslationInput {
seoTitle: String seoTitle: String
seoDescription: String seoDescription: String
name: String name: String
description: String description: JSONString
descriptionJson: JSONString
} }
scalar UUID scalar UUID
@ -5547,7 +5661,7 @@ type VoucherUpdate {
voucher: Voucher voucher: Voucher
} }
type Warehouse implements Node { type Warehouse implements Node & ObjectWithMetadata {
id: ID! id: ID!
name: String! name: String!
slug: String! slug: String!
@ -5555,6 +5669,8 @@ type Warehouse implements Node {
shippingZones(before: String, after: String, first: Int, last: Int): ShippingZoneCountableConnection! shippingZones(before: String, after: String, first: Int, last: Int): ShippingZoneCountableConnection!
address: Address! address: Address!
email: String! email: String!
privateMetadata: [MetadataItem]!
metadata: [MetadataItem]!
} }
input WarehouseAddressInput { input WarehouseAddressInput {

View file

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

View file

@ -1,5 +1,6 @@
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
@ -7,16 +8,90 @@ import FormSpacer from "@saleor/components/FormSpacer";
import SingleSelectField from "@saleor/components/SingleSelectField"; import SingleSelectField from "@saleor/components/SingleSelectField";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import { commonMessages } from "@saleor/intl"; 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 { getFormErrors } from "@saleor/utils/errors";
import getAttributeErrorMessage from "@saleor/utils/errors/attribute"; import getAttributeErrorMessage from "@saleor/utils/errors/attribute";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { defineMessages, useIntl } from "react-intl";
import slugify from "slugify"; import slugify from "slugify";
import { getAttributeSlugErrorMessage } from "../../errors"; import { getAttributeSlugErrorMessage } from "../../errors";
import { AttributePageFormData } from "../AttributePage"; 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 { export interface AttributeDetailsProps {
canChangeType: boolean; canChangeType: boolean;
data: AttributePageFormData; data: AttributePageFormData;
@ -25,39 +100,43 @@ export interface AttributeDetailsProps {
onChange: (event: React.ChangeEvent<any>) => void; onChange: (event: React.ChangeEvent<any>) => void;
} }
const AttributeDetails: React.FC<AttributeDetailsProps> = ({ const AttributeDetails: React.FC<AttributeDetailsProps> = props => {
canChangeType, const { canChangeType, data, disabled, errors, onChange } = props;
data, const classes = useStyles(props);
disabled,
errors,
onChange
}) => {
const intl = useIntl(); const intl = useIntl();
const inputTypeChoices = [ const inputTypeChoices = [
{ {
label: intl.formatMessage({ label: intl.formatMessage(inputTypeMessages.dropdown),
defaultMessage: "Dropdown",
description: "product attribute type"
}),
value: AttributeInputTypeEnum.DROPDOWN value: AttributeInputTypeEnum.DROPDOWN
}, },
{ {
label: intl.formatMessage({ label: intl.formatMessage(inputTypeMessages.multiselect),
defaultMessage: "Multiple Select",
description: "product attribute type"
}),
value: AttributeInputTypeEnum.MULTISELECT value: AttributeInputTypeEnum.MULTISELECT
}, },
{ {
label: intl.formatMessage({ label: intl.formatMessage(inputTypeMessages.file),
defaultMessage: "File",
description: "file attribute type"
}),
value: AttributeInputTypeEnum.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 ( return (
<Card> <Card>
@ -68,10 +147,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({
<TextField <TextField
disabled={disabled} disabled={disabled}
error={!!formErrors.name} error={!!formErrors.name}
label={intl.formatMessage({ label={intl.formatMessage(messages.attributeLabel)}
defaultMessage: "Default Label",
description: "attribute's label"
})}
name={"name" as keyof AttributePageFormData} name={"name" as keyof AttributePageFormData}
fullWidth fullWidth
helperText={getAttributeErrorMessage(formErrors.name, intl)} helperText={getAttributeErrorMessage(formErrors.name, intl)}
@ -82,45 +158,46 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({
<TextField <TextField
disabled={disabled} disabled={disabled}
error={!!formErrors.slug} error={!!formErrors.slug}
label={intl.formatMessage({ label={intl.formatMessage(messages.attributeSlug)}
defaultMessage: "Attribute Code",
description: "attribute's slug short code label"
})}
name={"slug" as keyof AttributePageFormData} name={"slug" as keyof AttributePageFormData}
placeholder={slugify(data.name).toLowerCase()} placeholder={slugify(data.name).toLowerCase()}
fullWidth fullWidth
helperText={ helperText={
getAttributeSlugErrorMessage(formErrors.slug, intl) || getAttributeSlugErrorMessage(formErrors.slug, intl) ||
intl.formatMessage({ intl.formatMessage(messages.attributeSlugHelperText)
defaultMessage:
"This is used internally. Make sure you dont use spaces",
description: "attribute slug input field helper text"
})
} }
value={data.slug} value={data.slug}
onChange={onChange} onChange={onChange}
/> />
<FormSpacer /> <FormSpacer />
<SingleSelectField <div className={classes.inputTypeSection}>
choices={inputTypeChoices} <SingleSelectField
disabled={disabled || !canChangeType} choices={inputTypeChoices}
error={!!formErrors.inputType} disabled={disabled || !canChangeType}
hint={getAttributeErrorMessage(formErrors.inputType, intl)} error={!!formErrors.inputType}
label={intl.formatMessage({ hint={getAttributeErrorMessage(formErrors.inputType, intl)}
defaultMessage: "Catalog Input type for Store Owner", label={intl.formatMessage(messages.inputType)}
description: "attribute's editor component" name="inputType"
})} onChange={onChange}
name="inputType" value={data.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 /> <FormSpacer />
<ControlledCheckbox <ControlledCheckbox
name={"valueRequired" as keyof AttributePageFormData} name={"valueRequired" as keyof AttributePageFormData}
label={intl.formatMessage({ label={intl.formatMessage(messages.valueRequired)}
defaultMessage: "Value Required",
description: "check to require attribute to have value"
})}
checked={data.valueRequired} checked={data.valueRequired}
onChange={onChange} onChange={onChange}
disabled={disabled} 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 AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer"; import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
@ -17,6 +18,7 @@ import { sectionNames } from "@saleor/intl";
import { maybe } from "@saleor/misc"; import { maybe } from "@saleor/misc";
import { ReorderAction } from "@saleor/types"; import { ReorderAction } from "@saleor/types";
import { import {
AttributeEntityTypeEnum,
AttributeInputTypeEnum, AttributeInputTypeEnum,
AttributeTypeEnum AttributeTypeEnum
} from "@saleor/types/globalTypes"; } from "@saleor/types/globalTypes";
@ -51,6 +53,7 @@ export interface AttributePageFormData extends MetadataFormData {
availableInGrid: boolean; availableInGrid: boolean;
filterableInDashboard: boolean; filterableInDashboard: boolean;
inputType: AttributeInputTypeEnum; inputType: AttributeInputTypeEnum;
entityType: AttributeEntityTypeEnum;
filterableInStorefront: boolean; filterableInStorefront: boolean;
name: string; name: string;
slug: string; slug: string;
@ -84,6 +87,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
attribute === null attribute === null
? { ? {
availableInGrid: true, availableInGrid: true,
entityType: null,
filterableInDashboard: true, filterableInDashboard: true,
filterableInStorefront: true, filterableInStorefront: true,
inputType: AttributeInputTypeEnum.DROPDOWN, inputType: AttributeInputTypeEnum.DROPDOWN,
@ -98,6 +102,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
} }
: { : {
availableInGrid: maybe(() => attribute.availableInGrid, true), availableInGrid: maybe(() => attribute.availableInGrid, true),
entityType: attribute?.entityType ?? null,
filterableInDashboard: maybe( filterableInDashboard: maybe(
() => attribute.filterableInDashboard, () => attribute.filterableInDashboard,
true true
@ -172,7 +177,9 @@ const AttributePage: React.FC<AttributePageProps> = ({
errors={errors} errors={errors}
onChange={change} onChange={change}
/> />
{data.inputType !== AttributeInputTypeEnum.FILE && ( {ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes(
data.inputType
) && (
<> <>
<CardSpacer /> <CardSpacer />
<AttributeValues <AttributeValues

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
/* eslint-disable */ /* eslint-disable */
// This file was automatically generated and should not be edited. // 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 // GraphQL mutation operation: AttributeValueUpdate
@ -32,6 +32,7 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute_values {
name: string | null; name: string | null;
slug: string | null; slug: string | null;
file: AttributeValueUpdate_attributeValueUpdate_attribute_values_file | null; file: AttributeValueUpdate_attributeValueUpdate_attribute_values_file | null;
reference: string | null;
} }
export interface AttributeValueUpdate_attributeValueUpdate_attribute { export interface AttributeValueUpdate_attributeValueUpdate_attribute {
@ -47,6 +48,7 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute {
privateMetadata: (AttributeValueUpdate_attributeValueUpdate_attribute_privateMetadata | null)[]; privateMetadata: (AttributeValueUpdate_attributeValueUpdate_attribute_privateMetadata | null)[];
availableInGrid: boolean; availableInGrid: boolean;
inputType: AttributeInputTypeEnum | null; inputType: AttributeInputTypeEnum | null;
entityType: AttributeEntityTypeEnum | null;
storefrontSearchPosition: number; storefrontSearchPosition: number;
valueRequired: boolean; valueRequired: boolean;
values: (AttributeValueUpdate_attributeValueUpdate_attribute_values | null)[] | null; 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 { FileUpload } from "@saleor/files/types/FileUpload";
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment"; import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment"; import { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment";
import { UploadErrorFragment } from "@saleor/fragments/types/UploadErrorFragment"; import { UploadErrorFragment } from "@saleor/fragments/types/UploadErrorFragment";
import { FormsetData } from "@saleor/hooks/useFormset"; import { FormsetData } from "@saleor/hooks/useFormset";
import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails"; 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 { import {
AttributeEntityTypeEnum,
AttributeInputTypeEnum, AttributeInputTypeEnum,
AttributeValueInput AttributeValueInput
} from "@saleor/types/globalTypes"; } from "@saleor/types/globalTypes";
import { mapNodeToChoice, mapPagesToChoices } from "@saleor/utils/maps";
import { MutationFetchResult } from "react-apollo"; import { MutationFetchResult } from "react-apollo";
import { AttributePageFormData } from "../components/AttributePage";
import { AttributeValueEditDialogFormData } from "../components/AttributeValueEditDialog";
import { AttributeValueDelete } from "../types/AttributeValueDelete"; 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 = ( export const isFileValueUnused = (
attributesWithNewFileValue: FormsetData<null, File>, attributesWithNewFileValue: FormsetData<null, File>,
existingAttribute: existingAttribute:
@ -54,6 +125,18 @@ export const mergeAttributeValueDeleteErrors = (
return errors; 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 = ( export const getFileValuesToUploadFromAttributes = (
attributesWithNewFileValue: FormsetData<null, File> attributesWithNewFileValue: FormsetData<null, File>
) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value); ) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value);
@ -104,3 +187,131 @@ export const getAttributesAfterFileAttributesUpdate = (
return uploadedFileAttributes.concat(removedFileAttributes); 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 { AttributeInputData } from "@saleor/components/Attributes";
import { FormsetData } from "@saleor/hooks/useFormset"; import { FormsetData } from "@saleor/hooks/useFormset";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes"; import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
import { createAttributeMultiChangeHandler } from "./handlers";
const attributes: FormsetData<AttributeInputData, string[]> = [ const attributes: FormsetData<AttributeInputData, string[]> = [
{ {
data: { data: {
@ -15,6 +14,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
file: null, file: null,
id: "attrv-1", id: "attrv-1",
name: "Attribute 1 Value 1", name: "Attribute 1 Value 1",
reference: null,
slug: "attr-1-v-1" slug: "attr-1-v-1"
} }
] ]
@ -33,6 +33,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
file: null, file: null,
id: "attrv-2", id: "attrv-2",
name: "Attribute 2 Value 1", name: "Attribute 2 Value 1",
reference: null,
slug: "attr-2-v-1" slug: "attr-2-v-1"
}, },
{ {
@ -40,6 +41,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
file: null, file: null,
id: "attrv-3", id: "attrv-3",
name: "Attribute 2 Value 2", name: "Attribute 2 Value 2",
reference: null,
slug: "attr-2-v-2" slug: "attr-2-v-2"
}, },
{ {
@ -47,6 +49,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
file: null, file: null,
id: "attrv-4", id: "attrv-4",
name: "Attribute 2 Value 3", name: "Attribute 2 Value 3",
reference: null,
slug: "attr-2-v-3" slug: "attr-2-v-3"
} }
] ]
@ -69,6 +72,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
}, },
id: "gdghdgdhkkdae", id: "gdghdgdhkkdae",
name: "File First Value", name: "File First Value",
reference: null,
slug: "file-first-value" slug: "file-first-value"
} }
] ]

View file

@ -1,14 +1,24 @@
import { AttributeInput } from "@saleor/components/Attributes"; import {
AttributeInput,
AttributeInputData
} from "@saleor/components/Attributes";
import { import {
FileUpload, FileUpload,
FileUploadVariables FileUploadVariables
} from "@saleor/files/types/FileUpload"; } from "@saleor/files/types/FileUpload";
import { FormsetData } from "@saleor/hooks/useFormset";
import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails";
import { 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, AttributeInputTypeEnum,
AttributeValueInput AttributeValueInput
} from "@saleor/types/globalTypes"; } from "@saleor/types/globalTypes";
import { move, toggle } from "@saleor/utils/lists";
import { MutationFetchResult } from "react-apollo"; import { MutationFetchResult } from "react-apollo";
import { import {
@ -17,6 +27,149 @@ import {
} from "../types/AttributeValueDelete"; } from "../types/AttributeValueDelete";
import { getFileValuesToUploadFromAttributes, isFileValueUnused } from "./data"; 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 { interface AttributesArgs {
attributes: AttributeInput[]; attributes: AttributeInput[];
updatedFileAttributes: AttributeValueInput[]; updatedFileAttributes: AttributeValueInput[];
@ -44,6 +197,12 @@ export const prepareAttributesInput = ({
id: attribute.id id: attribute.id
}; };
} }
if (attribute.data.inputType === AttributeInputTypeEnum.REFERENCE) {
return {
id: attribute.id,
references: attribute.value
};
}
return { return {
id: attribute.id, id: attribute.id,
values: attribute.value[0] === "" ? [] : attribute.value 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 { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import { getStringOrPlaceholder } from "@saleor/misc"; import { getStringOrPlaceholder } from "@saleor/misc";
import { ReorderEvent } from "@saleor/types"; import { ReorderEvent } from "@saleor/types";
import { import { AttributeErrorCode } from "@saleor/types/globalTypes";
AttributeErrorCode,
AttributeInputTypeEnum
} from "@saleor/types/globalTypes";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler"; import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
import { import {
@ -57,33 +55,6 @@ function areValuesEqual(
return a.name === b.name; 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 AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
const navigate = useNavigator(); const navigate = useNavigator();
const notify = useNotifier(); const notify = useNotifier();
@ -145,10 +116,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
setValues(move(values[oldIndex], values, areValuesEqual, newIndex)); setValues(move(values[oldIndex], values, areValuesEqual, newIndex));
const handleCreate = async (data: AttributePageFormData) => { const handleCreate = async (data: AttributePageFormData) => {
const input = const input = getAttributeData(data, values);
data.inputType === AttributeInputTypeEnum.FILE
? getFileAttributeData(data, values)
: getSimpleAttributeData(data, values);
const result = await attributeCreate({ const result = await attributeCreate({
variables: { variables: {
@ -190,6 +158,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
__typename: "AttributeValue" as "AttributeValue", __typename: "AttributeValue" as "AttributeValue",
file: null, file: null,
id: valueIndex.toString(), id: valueIndex.toString(),
reference: null,
slug: slugify(value.name).toLowerCase(), slug: slugify(value.name).toLowerCase(),
sortOrder: valueIndex, sortOrder: valueIndex,
value: null, value: null,

View file

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

View file

@ -2,7 +2,7 @@ import setupApi from "@test/api";
import { act, renderHook } from "@testing-library/react-hooks"; import { act, renderHook } from "@testing-library/react-hooks";
import ApolloClient from "apollo-client"; import ApolloClient from "apollo-client";
import { useAuthProvider } from "./AuthProvider"; import { useAuthProvider } from "./hooks/useAuthProvider";
import { getTokens, setAuthToken } from "./utils"; import { getTokens, setAuthToken } from "./utils";
const apolloClient = setupApi(); const apolloClient = setupApi();
@ -14,18 +14,23 @@ function renderAuthProvider(apolloClient: ApolloClient<any>) {
const notify = jest.fn(); const notify = jest.fn();
const { result } = renderHook(() => const { result } = renderHook(() =>
useAuthProvider(intl as any, notify, apolloClient) useAuthProvider({ apolloClient, intl: intl as any, notify })
); );
return result; return result;
} }
const credentials = { const adminCredentials = {
email: "admin@example.com", email: "admin@example.com",
password: "admin", password: "admin",
token: null token: null
}; };
const nonStaffUserCredentials = {
email: "client@example.com",
password: "password"
};
beforeEach(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
@ -36,10 +41,10 @@ describe("User", () => {
const hook = renderAuthProvider(apolloClient); const hook = renderAuthProvider(apolloClient);
await act(() => await act(() =>
hook.current.login(credentials.email, credentials.password) hook.current.login(adminCredentials.email, adminCredentials.password)
); );
expect(hook.current.userContext.email).toBe(credentials.email); expect(hook.current.user.email).toBe(adminCredentials.email);
credentials.token = getTokens().auth; adminCredentials.token = getTokens().auth;
done(); done();
}); });
@ -48,19 +53,33 @@ describe("User", () => {
const hook = renderAuthProvider(apolloClient); const hook = renderAuthProvider(apolloClient);
await act(() => 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(); done();
}); });
it("will be logged if has valid token", async done => { it("will be logged if has valid token", async done => {
setAuthToken(credentials.token, false); setAuthToken(adminCredentials.token, false);
const hook = renderAuthProvider(apolloClient); const hook = renderAuthProvider(apolloClient);
await act(() => hook.current.autologinPromise.current); await act(() => hook.current.autologinPromise.current);
expect(hook.current.userContext.email).toBe(credentials.email); expect(hook.current.user.email).toBe(adminCredentials.email);
done(); done();
}); });
@ -70,7 +89,7 @@ describe("User", () => {
const hook = renderAuthProvider(apolloClient); const hook = renderAuthProvider(apolloClient);
await act(() => hook.current.autologinPromise.current); await act(() => hook.current.autologinPromise.current);
expect(hook.current.userContext).toBe(undefined); expect(hook.current.user).toBe(undefined);
done(); 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 useNotifier from "@saleor/hooks/useNotifier";
import { getMutationStatus } from "@saleor/misc"; import React, { useContext } from "react";
import { import { useApolloClient } from "react-apollo";
isSupported as isCredentialsManagementAPISupported, import { useIntl } from "react-intl";
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 { UserContext } from "./"; import { UserContext } from "./";
import { import { useAuthProvider } from "./hooks/useAuthProvider";
tokenAuthMutation, import { getTokens } from "./utils";
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
};
}
interface AuthProviderProps { interface AuthProviderProps {
children: React.ReactNode; children: React.ReactNode;
@ -184,30 +16,10 @@ const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const intl = useIntl(); const intl = useIntl();
const notify = useNotifier(); const notify = useNotifier();
const { const authProvider = useAuthProvider({ apolloClient, intl, notify });
login,
loginByToken,
logout,
tokenAuthOpts,
refreshToken,
tokenVerifyOpts,
userContext
} = useAuthProvider(intl, notify, apolloClient);
return ( return (
<UserContext.Provider <UserContext.Provider value={authProvider}>{children}</UserContext.Provider>
value={{
login,
loginByToken,
logout,
tokenAuthLoading: tokenAuthOpts.loading,
tokenRefresh: refreshToken,
tokenVerifyLoading: tokenVerifyOpts.loading,
user: userContext
}}
>
{children}
</UserContext.Provider>
); );
}; };

View file

@ -1,8 +1,8 @@
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import LoginLoading from "../../../auth/components/LoginLoading"; import LoginLoading from ".";
import Decorator from "../../Decorator";
storiesOf("Views / Authentication / Verifying remembered user", module) storiesOf("Views / Authentication / Verifying remembered user", module)
.addDecorator(Decorator) .addDecorator(Decorator)

View file

@ -1,14 +1,24 @@
import { Omit } from "@material-ui/core"; import { Omit } from "@material-ui/core";
import CardDecorator from "@saleor/storybook/CardDecorator";
import Decorator from "@saleor/storybook/Decorator";
import { storiesOf } from "@storybook/react"; import { storiesOf } from "@storybook/react";
import React from "react"; import React from "react";
import LoginPage, { LoginCardProps } from "../../../auth/components/LoginPage"; import LoginPage, { LoginCardProps } from "../../../auth/components/LoginPage";
import CardDecorator from "../../CardDecorator";
import Decorator from "../../Decorator";
const props: Omit<LoginCardProps, "classes"> = { const props: Omit<LoginCardProps, "classes"> = {
disableLoginButton: true, disabled: false,
error: false, error: false,
externalAuthentications: [
{
__typename: "ExternalAuthentication",
id: "auth.plugin.example",
name: "Example auth plugin"
}
],
externalError: false,
loading: false,
onExternalAuthentication: () => undefined,
onPasswordRecovery: undefined, onPasswordRecovery: undefined,
onSubmit: () => undefined onSubmit: () => undefined
}; };
@ -18,4 +28,5 @@ storiesOf("Views / Authentication / Log in", module)
.addDecorator(Decorator) .addDecorator(Decorator)
.add("default", () => <LoginPage {...props} />) .add("default", () => <LoginPage {...props} />)
.add("error", () => <LoginPage {...props} error={true} />) .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 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 { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography"; 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 { FormSpacer } from "@saleor/components/FormSpacer";
import { DEMO_MODE } from "@saleor/config"; import { SubmitPromise } from "@saleor/hooks/useForm";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
export interface FormData { import LoginForm, { LoginFormData } from "./form";
email: string;
password: string;
}
const useStyles = makeStyles( const useStyles = makeStyles(
theme => ({ theme => ({
@ -23,7 +22,13 @@ const useStyles = makeStyles(
link: { link: {
color: theme.palette.primary.main, color: theme.palette.primary.main,
cursor: "pointer", cursor: "pointer",
textAlign: "center" textDecoration: "underline"
},
loading: {
alignItems: "center",
display: "flex",
height: "100vh",
justifyContent: "center"
}, },
loginButton: { loginButton: {
width: 140 width: 140
@ -43,27 +48,40 @@ const useStyles = makeStyles(
export interface LoginCardProps { export interface LoginCardProps {
error: boolean; error: boolean;
disableLoginButton: boolean; externalError: boolean;
disabled: boolean;
loading: boolean;
externalAuthentications?: AvailableExternalAuthentications_shop_availableExternalAuthentications[];
onExternalAuthentication: (pluginId: string) => void;
onPasswordRecovery: () => void; onPasswordRecovery: () => void;
onSubmit?(event: FormData); onSubmit?: (event: LoginFormData) => SubmitPromise;
} }
const LoginCard: React.FC<LoginCardProps> = props => { 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 classes = useStyles(props);
const intl = useIntl(); const intl = useIntl();
let initialFormData = { email: "", password: "" }; if (loading) {
if (DEMO_MODE) { return (
initialFormData = { <div className={classes.loading}>
email: "admin@example.com", <CircularProgress size={128} />
password: "admin" </div>
}; );
} }
return ( return (
<Form initial={initialFormData} onSubmit={onSubmit}> <LoginForm onSubmit={onSubmit}>
{({ change: handleChange, data, submit: handleSubmit }) => ( {({ change: handleChange, data, submit: handleSubmit }) => (
<> <>
{error && ( {error && (
@ -73,6 +91,13 @@ const LoginCard: React.FC<LoginCardProps> = props => {
</Typography> </Typography>
</div> </div>
)} )}
{externalError && (
<div className={classes.panel} data-test="loginErrorMessage">
<Typography variant="caption">
<FormattedMessage defaultMessage="Sorry, login went wrong. Please try again." />
</Typography>
</div>
)}
<TextField <TextField
autoFocus autoFocus
fullWidth fullWidth
@ -84,6 +109,7 @@ const LoginCard: React.FC<LoginCardProps> = props => {
inputProps={{ inputProps={{
"data-test": "email" "data-test": "email"
}} }}
disabled={disabled}
/> />
<FormSpacer /> <FormSpacer />
<TextField <TextField
@ -99,13 +125,14 @@ const LoginCard: React.FC<LoginCardProps> = props => {
inputProps={{ inputProps={{
"data-test": "password" "data-test": "password"
}} }}
disabled={disabled}
/> />
<FormSpacer /> <FormSpacer />
<div className={classes.buttonContainer}> <div className={classes.buttonContainer}>
<Button <Button
className={classes.loginButton} className={classes.loginButton}
color="primary" color="primary"
disabled={disableLoginButton} disabled={disabled}
variant="contained" variant="contained"
onClick={handleSubmit} onClick={handleSubmit}
type="submit" type="submit"
@ -115,15 +142,56 @@ const LoginCard: React.FC<LoginCardProps> = props => {
</Button> </Button>
</div> </div>
<FormSpacer /> <FormSpacer />
<Typography className={classes.link} onClick={onPasswordRecovery}> <Typography>
<FormattedMessage <FormattedMessage
defaultMessage="Reset your password" defaultMessage="Forgot password? {resetPasswordLink}"
description="button" description="description"
values={{
resetPasswordLink: (
<a className={classes.link} onClick={onPasswordRecovery}>
<FormattedMessage
defaultMessage="Use this link to recover it"
description="link"
/>
</a>
)
}}
/> />
</Typography> </Typography>
{externalAuthentications.length > 0 && (
<>
<FormSpacer />
<Divider />
<FormSpacer />
<Typography>
<FormattedMessage
defaultMessage="or login using"
description="description"
/>
</Typography>
</>
)}
{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>
))}
</> </>
)} )}
</Form> </LoginForm>
); );
}; };
LoginCard.displayName = "LoginCard"; 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 { User } from "@saleor/fragments/types/User";
import React from "react"; import { parse as parseQs } from "qs";
import { Route, Switch } from "react-router-dom"; import React, { MutableRefObject } from "react";
import { Route, RouteComponentProps, Switch } from "react-router-dom";
import Layout from "./components/Layout"; import Layout from "./components/Layout";
import { import {
ExternalLoginInput,
RequestExternalLoginInput
} from "./hooks/useExternalAuthProvider";
import { ExternalObtainAccessTokens_externalObtainAccessTokens } from "./types/ExternalObtainAccessTokens";
import { TokenAuth_tokenCreate } from "./types/TokenAuth";
import {
LoginUrlQueryParams,
newPasswordPath, newPasswordPath,
passwordResetPath, passwordResetPath,
passwordResetSuccessPath passwordResetSuccessPath
} from "./urls"; } from "./urls";
import LoginView from "./views/Login"; import LoginViewComponent from "./views/Login";
import NewPassword from "./views/NewPassword"; import NewPassword from "./views/NewPassword";
import ResetPassword from "./views/ResetPassword"; import ResetPassword from "./views/ResetPassword";
import ResetPasswordSuccess from "./views/ResetPasswordSuccess"; 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 { 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; loginByToken: (auth: string, csrf: string, user: User) => void;
logout: () => void; logout: () => void;
requestLoginByExternalPlugin: (
pluginId: string,
input: RequestExternalLoginInput
) => Promise<void>;
tokenAuthLoading: boolean; tokenAuthLoading: boolean;
tokenRefresh: () => Promise<boolean>; tokenRefresh: () => Promise<boolean>;
tokenVerifyLoading: boolean; tokenVerifyLoading: boolean;
user?: User; user?: User;
autologinPromise?: MutableRefObject<Promise<any>>;
} }
export const UserContext = React.createContext<UserContext>({ export const UserContext = React.createContext<UserContext>({
login: undefined, login: undefined,
loginByExternalPlugin: undefined,
loginByToken: undefined, loginByToken: undefined,
logout: undefined, logout: undefined,
requestLoginByExternalPlugin: undefined,
tokenAuthLoading: false, tokenAuthLoading: false,
tokenRefresh: undefined, tokenRefresh: undefined,
tokenVerifyLoading: false tokenVerifyLoading: false

View file

@ -82,3 +82,52 @@ export const SetPasswordMutation = TypedMutation<
SetPassword, SetPassword,
SetPasswordVariables SetPasswordVariables
>(setPassword); >(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; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
isStaff: boolean;
userPermissions: (SetPassword_setPassword_user_userPermissions | null)[] | null; userPermissions: (SetPassword_setPassword_user_userPermissions | null)[] | null;
avatar: SetPassword_setPassword_user_avatar | null; avatar: SetPassword_setPassword_user_avatar | null;
} }

View file

@ -31,6 +31,7 @@ export interface TokenAuth_tokenCreate_user {
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
isStaff: boolean;
userPermissions: (TokenAuth_tokenCreate_user_userPermissions | null)[] | null; userPermissions: (TokenAuth_tokenCreate_user_userPermissions | null)[] | null;
avatar: TokenAuth_tokenCreate_user_avatar | null; avatar: TokenAuth_tokenCreate_user_avatar | null;
} }

View file

@ -25,6 +25,7 @@ export interface VerifyToken_tokenVerify_user {
email: string; email: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
isStaff: boolean;
userPermissions: (VerifyToken_tokenVerify_user_userPermissions | null)[] | null; userPermissions: (VerifyToken_tokenVerify_user_userPermissions | null)[] | null;
avatar: VerifyToken_tokenVerify_user_avatar | 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 passwordResetSuccessUrl = passwordResetSuccessPath;
export const newPasswordPath = "/new-password/"; export const newPasswordPath = "/new-password/";
export const loginCallbackPath = "/login/callback/";
export interface NewPasswordUrlQueryParams { export interface NewPasswordUrlQueryParams {
email: string; email: string;
token: string; token: string;
} }
export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) => export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) =>
newPasswordPath + "?" + stringifyQs(params); 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 useNavigator from "@saleor/hooks/useNavigator";
import useUser from "@saleor/hooks/useUser"; 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 LoginPage from "../components/LoginPage";
import { passwordResetUrl } from "../urls"; 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 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 ( return (
<LoginPage <LoginPage
error={user === null} error={isError}
disableLoginButton={tokenAuthLoading} externalError={isExternalError}
disabled={tokenAuthLoading}
externalAuthentications={
externalAuthentications?.shop?.availableExternalAuthentications
}
loading={externalAuthenticationsLoading || tokenAuthLoading}
onExternalAuthentication={handleRequestExternalAuthentication}
onPasswordRecovery={() => navigate(passwordResetUrl)} onPasswordRecovery={() => navigate(passwordResetUrl)}
onSubmit={handleSubmit} onSubmit={handleSubmit}
/> />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,6 +20,7 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useStyles } from "../styles"; import { useStyles } from "../styles";
import { ExtendedFormHelperTextProps } from "./types";
export interface FormData { export interface FormData {
name: string; name: string;
@ -80,6 +81,11 @@ export const ChannelForm: React.FC<ChannelFormProps> = ({
helperText={getChannelsErrorMessage(formErrors?.slug, intl)} helperText={getChannelsErrorMessage(formErrors?.slug, intl)}
disabled={disabled} disabled={disabled}
fullWidth fullWidth
FormHelperTextProps={
{
"data-testid": "slug-text-input-helper-text"
} as ExtendedFormHelperTextProps
}
label={intl.formatMessage({ label={intl.formatMessage({
defaultMessage: "Slug", defaultMessage: "Slug",
description: "channel slug" description: "channel slug"
@ -124,8 +130,14 @@ export const ChannelForm: React.FC<ChannelFormProps> = ({
<CardContent> <CardContent>
{!!currencyCodes ? ( {!!currencyCodes ? (
<SingleAutocompleteSelectField <SingleAutocompleteSelectField
data-test-id="channel-currency-select-input"
allowCustomValues allowCustomValues
error={!!formErrors.currencyCode} error={!!formErrors.currencyCode}
FormHelperTextProps={
{
"data-testid": "currency-text-input-helper-text"
} as ExtendedFormHelperTextProps
}
helperText={getChannelsErrorMessage( helperText={getChannelsErrorMessage(
formErrors?.currencyCode, formErrors?.currencyCode,
intl 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 makeQuery from "@saleor/hooks/makeQuery";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { BaseChannels } from "./types/BaseChannels";
import { Channel, ChannelVariables } from "./types/Channel"; import { Channel, ChannelVariables } from "./types/Channel";
import { Channels } from "./types/Channels"; import { Channels } from "./types/Channels";
export const channelsListBase = gql`
${channelFragment}
query BaseChannels {
channels {
...ChannelFragment
}
}
`;
export const channelsList = gql` export const channelsList = gql`
${channelDetailsFragment} ${channelDetailsFragment}
query Channels { query Channels {
@ -23,6 +36,9 @@ export const channelDetails = gql`
} }
`; `;
export const useBaseChannelsList = makeQuery<BaseChannels, {}>(
channelsListBase
);
export const useChannelsList = makeQuery<Channels, {}>(channelsList); export const useChannelsList = makeQuery<Channels, {}>(channelsList);
export const useChannelDetails = makeQuery<Channel, ChannelVariables>( export const useChannelDetails = makeQuery<Channel, ChannelVariables>(
channelDetails 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 intl = useIntl();
const formErrors = getFormErrors(["name", "descriptionJson"], errors); const formErrors = getFormErrors(["name", "description"], errors);
return ( return (
<Card> <Card>
@ -57,8 +57,8 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
<FormSpacer /> <FormSpacer />
<RichTextEditor <RichTextEditor
data={data.description} data={data.description}
error={!!formErrors.descriptionJson} error={!!formErrors.description}
helperText={getProductErrorMessage(formErrors.descriptionJson, intl)} helperText={getProductErrorMessage(formErrors.description, intl)}
label={intl.formatMessage(commonMessages.description)} label={intl.formatMessage(commonMessages.description)}
name="description" name="description"
disabled={disabled} disabled={disabled}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -102,7 +102,7 @@ export const CollectionCreate: React.FC<CollectionCreateProps> = ({
input: { input: {
backgroundImage: formData.backgroundImage.value, backgroundImage: formData.backgroundImage.value,
backgroundImageAlt: formData.backgroundImageAlt, backgroundImageAlt: formData.backgroundImageAlt,
descriptionJson: JSON.stringify(formData.description), description: JSON.stringify(formData.description),
name: formData.name, name: formData.name,
seo: { seo: {
description: formData.seoDescription, description: formData.seoDescription,

View file

@ -198,7 +198,7 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
const handleUpdate = async (formData: CollectionUpdateData) => { const handleUpdate = async (formData: CollectionUpdateData) => {
const input: CollectionInput = { const input: CollectionInput = {
backgroundImageAlt: formData.backgroundImageAlt, backgroundImageAlt: formData.backgroundImageAlt,
descriptionJson: JSON.stringify(formData.description), description: JSON.stringify(formData.description),
name: formData.name, name: formData.name,
seo: { seo: {
description: formData.seoDescription, description: formData.seoDescription,

View file

@ -61,7 +61,11 @@ const AppHeader: React.FC<AppHeaderProps> = props => {
{anchor => {anchor =>
anchor ? ( anchor ? (
<Portal container={anchor.current}> <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} /> <ArrowBackIcon className={classes.backArrow} />
{children ? ( {children ? (
<Typography className={classes.title}>{children}</Typography> <Typography className={classes.title}>{children}</Typography>

View file

@ -1,12 +1,12 @@
import { useAuth } from "@saleor/auth/AuthProvider"; import { useAuth } from "@saleor/auth/AuthProvider";
import { useChannelsList } from "@saleor/channels/queries"; import { useBaseChannelsList } from "@saleor/channels/queries";
import { ChannelDetailsFragment } from "@saleor/fragments/types/ChannelDetailsFragment"; import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment";
import useLocalStorage from "@saleor/hooks/useLocalStorage"; import useLocalStorage from "@saleor/hooks/useLocalStorage";
import React from "react"; import React from "react";
interface UseAppChannel { interface UseAppChannel {
availableChannels: ChannelDetailsFragment[]; availableChannels: ChannelFragment[];
channel: ChannelDetailsFragment; channel: ChannelFragment;
isPickerActive: boolean; isPickerActive: boolean;
refreshChannels: () => void; refreshChannels: () => void;
setChannel: (id: string) => void; setChannel: (id: string) => void;
@ -27,14 +27,14 @@ const AppChannelContext = React.createContext<AppChannelContextData>({
export const AppChannelProvider: React.FC = ({ children }) => { export const AppChannelProvider: React.FC = ({ children }) => {
const { isAuthenticated } = useAuth(); const { isAuthenticated } = useAuth();
const [selectedChannel, setSelectedChannel] = useLocalStorage("channel", ""); const [selectedChannel, setSelectedChannel] = useLocalStorage("channel", "");
const { data: channelData, refetch } = useChannelsList({ const { data: channelData, refetch } = useBaseChannelsList({
skip: !isAuthenticated skip: !isAuthenticated
}); });
const [isPickerActive, setPickerActive] = React.useState(false); const [isPickerActive, setPickerActive] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
if (!selectedChannel) { if (!selectedChannel && channelData?.channels) {
setSelectedChannel(channelData?.channels[0].id); setSelectedChannel(channelData.channels[0].id);
} }
}, [channelData]); }, [channelData]);

View file

@ -1,5 +1,5 @@
import makeStyles from "@material-ui/core/styles/makeStyles"; 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 { ChannelProps } from "@saleor/types";
import { mapNodeToChoice } from "@saleor/utils/maps"; import { mapNodeToChoice } from "@saleor/utils/maps";
import React from "react"; import React from "react";
@ -22,7 +22,7 @@ const useStyles = makeStyles(
); );
export interface AppChannelSelectProps extends ChannelProps { export interface AppChannelSelectProps extends ChannelProps {
channels: ChannelDetailsFragment[]; channels: ChannelFragment[];
disabled: boolean; disabled: boolean;
onChannelSelect: (id: string) => void; onChannelSelect: (id: string) => void;
} }

View file

@ -239,8 +239,11 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
</div> </div>
<main className={classes.view}> <main className={classes.view}>
{appState.error {appState.error
? appState.error === "unhandled" && ( ? appState.error.type === "unhandled" && (
<ErrorPage onBack={handleErrorBack} /> <ErrorPage
id={appState.error.id}
onBack={handleErrorBack}
/>
) )
: children} : children}
</main> </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, loading: false,
onChange: () => undefined, onChange: () => undefined,
onFileChange: () => undefined, onFileChange: () => undefined,
onMultiChange: () => undefined onMultiChange: () => undefined,
onReferencesAddClick: () => undefined,
onReferencesRemove: () => undefined,
onReferencesReorder: () => undefined
}; };
storiesOf("Attributes / Attributes", module) 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 makeStyles from "@material-ui/core/styles/makeStyles";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
import { AttributeReference } from "@saleor/attributes/utils/data";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Grid from "@saleor/components/Grid";
import Hr from "@saleor/components/Hr"; import Hr from "@saleor/components/Hr";
import MultiAutocompleteSelectField, { import MultiAutocompleteSelectField, {
MultiAutocompleteChoiceType MultiAutocompleteChoiceType
@ -17,7 +17,11 @@ import { AttributeValueFragment } from "@saleor/fragments/types/AttributeValueFr
import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment"; import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment";
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset"; 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 { getProductErrorMessage } from "@saleor/utils/errors";
import getPageErrorMessage from "@saleor/utils/errors/page"; import getPageErrorMessage from "@saleor/utils/errors/page";
import classNames from "classnames"; import classNames from "classnames";
@ -30,14 +34,21 @@ import {
} from "react-intl"; } from "react-intl";
import FileUploadField, { FileChoiceType } from "../FileUploadField"; import FileUploadField, { FileChoiceType } from "../FileUploadField";
import SortableChipsField, {
SortableChipsFieldValueType
} from "../SortableChipsField";
import BasicAttributeRow from "./BasicAttributeRow";
import ExtendedAttributeRow from "./ExtendedAttributeRow";
import { VariantAttributeScope } from "./types"; import { VariantAttributeScope } from "./types";
export interface AttributeInputData { export interface AttributeInputData {
inputType: AttributeInputTypeEnum; inputType: AttributeInputTypeEnum;
entityType?: AttributeEntityTypeEnum;
variantAttributeScope?: VariantAttributeScope; variantAttributeScope?: VariantAttributeScope;
isRequired: boolean; isRequired: boolean;
values: AttributeValueFragment[]; values: AttributeValueFragment[];
selectedValues?: AttributeValueFragment[]; selectedValues?: AttributeValueFragment[];
references?: AttributeReference[];
} }
export type AttributeInput = FormsetAtomicData<AttributeInputData, string[]>; export type AttributeInput = FormsetAtomicData<AttributeInputData, string[]>;
export type AttributeFileInput = FormsetAtomicData<AttributeInputData, File[]>; export type AttributeFileInput = FormsetAtomicData<AttributeInputData, File[]>;
@ -49,9 +60,12 @@ export interface AttributesProps {
ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment
>; >;
title?: React.ReactNode; title?: React.ReactNode;
onChange: FormsetChange; onChange: FormsetChange<string>;
onMultiChange: FormsetChange; onMultiChange: FormsetChange<string>;
onFileChange?: FormsetChange; // TODO: temporairy optional, should be changed to required, after all pages implement it onFileChange: FormsetChange<File>;
onReferencesRemove: FormsetChange<string[]>;
onReferencesAddClick: (attribute: AttributeInput) => void;
onReferencesReorder: FormsetChange<ReorderEvent>;
} }
const useStyles = makeStyles( const useStyles = makeStyles(
@ -123,6 +137,10 @@ function getMultiChoices(
function getMultiDisplayValue( function getMultiDisplayValue(
attribute: AttributeInput attribute: AttributeInput
): MultiAutocompleteChoiceType[] { ): MultiAutocompleteChoiceType[] {
if (!attribute.value) {
return [];
}
return attribute.value.map(attributeValue => { return attribute.value.map(attributeValue => {
const definedAttributeValue = attribute.data.values.find( const definedAttributeValue = attribute.data.values.find(
definedValue => definedValue.slug === attributeValue 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( function getSingleChoices(
values: AttributeValueFragment[] values: AttributeValueFragment[]
): SingleAutocompleteChoiceType[] { ): SingleAutocompleteChoiceType[] {
@ -151,7 +203,7 @@ function getSingleChoices(
} }
function getFileChoice(attribute: AttributeInput): FileChoiceType { 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( const definedAttributeValue = attribute.data.values.find(
definedValue => definedValue.slug === attributeValue definedValue => definedValue.slug === attributeValue
@ -210,7 +262,10 @@ const Attributes: React.FC<AttributesProps> = ({
title, title,
onChange, onChange,
onMultiChange, onMultiChange,
onFileChange onFileChange,
onReferencesRemove,
onReferencesAddClick,
onReferencesReorder
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const classes = useStyles({}); const classes = useStyles({});
@ -255,76 +310,95 @@ const Attributes: React.FC<AttributesProps> = ({
return ( return (
<React.Fragment key={attribute.id}> <React.Fragment key={attribute.id}>
{attributeIndex > 0 && <Hr />} {attributeIndex > 0 && <Hr />}
<Grid className={classes.attributeSection} variant="uniform"> {attribute.data.inputType ===
<div AttributeInputTypeEnum.REFERENCE ? (
className={classes.attributeSectionLabel} <ExtendedAttributeRow
data-test="attribute-label" label={attribute.label}
selectLabel={intl.formatMessage({
defaultMessage: "Assign references",
description: "button label"
})}
onSelect={() => onReferencesAddClick(attribute)}
disabled={disabled}
> >
<Typography>{attribute.label}</Typography> <SortableChipsField
</div> values={getReferenceDisplayValue(attribute)}
<div data-test="attribute-value"> onValueDelete={value =>
{attribute.data.inputType === onReferencesRemove(
AttributeInputTypeEnum.FILE ? ( attribute.id,
<FileUploadField attribute.value?.filter(id => id !== value)
className={classes.fileField} )
disabled={disabled} }
loading={loading} onValueReorder={event =>
file={getFileChoice(attribute)} onReferencesReorder(attribute.id, event)
onFileUpload={file => }
onFileChange(attribute.id, file) loading={loading}
} error={!!error}
onFileDelete={() => helperText={getErrorMessage(error, intl)}
onFileChange(attribute.id, undefined) />
} </ExtendedAttributeRow>
error={!!error} ) : attribute.data.inputType ===
helperText={getErrorMessage(error, intl)} AttributeInputTypeEnum.FILE ? (
inputProps={{ <BasicAttributeRow label={attribute.label}>
name: `attribute:${attribute.label}` <FileUploadField
}} className={classes.fileField}
/> disabled={disabled}
) : attribute.data.inputType === loading={loading}
AttributeInputTypeEnum.DROPDOWN ? ( file={getFileChoice(attribute)}
<SingleAutocompleteSelectField onFileUpload={file => onFileChange(attribute.id, file)}
choices={getSingleChoices(attribute.data.values)} onFileDelete={() =>
disabled={disabled} onFileChange(attribute.id, undefined)
displayValue={ }
attribute.data.values.find( error={!!error}
value => value.slug === attribute.value[0] helperText={getErrorMessage(error, intl)}
)?.name || inputProps={{
attribute.value[0] || name: `attribute:${attribute.label}`
"" }}
} />
emptyOption={!attribute.data.isRequired} </BasicAttributeRow>
error={!!error} ) : attribute.data.inputType ===
helperText={getErrorMessage(error, intl)} AttributeInputTypeEnum.DROPDOWN ? (
name={`attribute:${attribute.label}`} <BasicAttributeRow label={attribute.label}>
label={intl.formatMessage(messages.valueLabel)} <SingleAutocompleteSelectField
value={attribute.value[0]} choices={getSingleChoices(attribute.data.values)}
onChange={event => disabled={disabled}
onChange(attribute.id, event.target.value) displayValue={
} attribute.data.values.find(
allowCustomValues={!attribute.data.isRequired} value => value.slug === attribute.value[0]
/> )?.name ||
) : ( attribute.value[0] ||
<MultiAutocompleteSelectField ""
choices={getMultiChoices(attribute.data.values)} }
displayValues={getMultiDisplayValue(attribute)} emptyOption={!attribute.data.isRequired}
disabled={disabled} error={!!error}
error={!!error} helperText={getErrorMessage(error, intl)}
helperText={getErrorMessage(error, intl)} name={`attribute:${attribute.label}`}
label={intl.formatMessage( label={intl.formatMessage(messages.valueLabel)}
messages.multipleValueLable value={attribute.value[0]}
)} onChange={event =>
name={`attribute:${attribute.label}`} onChange(attribute.id, event.target.value)
value={attribute.value} }
onChange={event => allowCustomValues={!attribute.data.isRequired}
onMultiChange(attribute.id, event.target.value) />
} </BasicAttributeRow>
allowCustomValues={!attribute.data.isRequired} ) : (
/> <BasicAttributeRow label={attribute.label}>
)} <MultiAutocompleteSelectField
</div> choices={getMultiChoices(attribute.data.values)}
</Grid> displayValues={getMultiDisplayValue(attribute)}
disabled={disabled}
error={!!error}
helperText={getErrorMessage(error, intl)}
label={intl.formatMessage(messages.multipleValueLable)}
name={`attribute:${attribute.label}`}
value={attribute.value}
onChange={event =>
onMultiChange(attribute.id, event.target.value)
}
allowCustomValues={!attribute.data.isRequired}
/>
</BasicAttributeRow>
)}
</React.Fragment> </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"; import { AttributeInput } from "./Attributes";
@ -12,6 +15,7 @@ const DROPDOWN_ATTRIBUTE: AttributeInput = {
file: null, file: null,
id: "fdinugiffgffd", id: "fdinugiffgffd",
name: "Dropdown First Value", name: "Dropdown First Value",
reference: null,
slug: "dropdown-first-value" slug: "dropdown-first-value"
}, },
{ {
@ -19,6 +23,7 @@ const DROPDOWN_ATTRIBUTE: AttributeInput = {
file: null, file: null,
id: "fdhfdhdihidff", id: "fdhfdhdihidff",
name: "Dropdown Second Value", name: "Dropdown Second Value",
reference: null,
slug: "dropdown-second-value" slug: "dropdown-second-value"
} }
] ]
@ -38,6 +43,7 @@ const MULTISELECT_ATTRIBUTE: AttributeInput = {
file: null, file: null,
id: "terteretregtt", id: "terteretregtt",
name: "Multiselect First Value", name: "Multiselect First Value",
reference: null,
slug: "multiselect-first-value" slug: "multiselect-first-value"
}, },
{ {
@ -45,6 +51,7 @@ const MULTISELECT_ATTRIBUTE: AttributeInput = {
file: null, file: null,
id: "tyueyryetopwr", id: "tyueyryetopwr",
name: "Multiselect Second Value", name: "Multiselect Second Value",
reference: null,
slug: "multiselect-second-value" slug: "multiselect-second-value"
}, },
{ {
@ -52,6 +59,7 @@ const MULTISELECT_ATTRIBUTE: AttributeInput = {
file: null, file: null,
id: "truiwrtweirqd", id: "truiwrtweirqd",
name: "Multiselect Third Value", name: "Multiselect Third Value",
reference: null,
slug: "multiselect-third-value" slug: "multiselect-third-value"
} }
] ]
@ -75,19 +83,72 @@ const FILE_ATTRIBUTE: AttributeInput = {
}, },
id: "gdghdgdhkkdae", id: "gdghdgdhkkdae",
name: "File First Value", name: "File First Value",
reference: null,
slug: "file-first-value" slug: "file-first-value"
} }
] ]
}, },
id: "ifudbgidfsb", id: "fguygygugyu",
label: "File Attribute", label: "File Attribute",
value: [] 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[] = [ export const ATTRIBUTES: AttributeInput[] = [
DROPDOWN_ATTRIBUTE, DROPDOWN_ATTRIBUTE,
MULTISELECT_ATTRIBUTE, MULTISELECT_ATTRIBUTE,
FILE_ATTRIBUTE FILE_ATTRIBUTE,
REFERENCE_ATTRIBUTE
]; ];
export const ATTRIBUTES_SELECTED: AttributeInput[] = [ export const ATTRIBUTES_SELECTED: AttributeInput[] = [
@ -105,5 +166,13 @@ export const ATTRIBUTES_SELECTED: AttributeInput[] = [
{ {
...FILE_ATTRIBUTE, ...FILE_ATTRIBUTE,
value: [FILE_ATTRIBUTE.data.values[0].slug] 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", email: "email@example.com",
firstName: "User", firstName: "User",
id: "123", id: "123",
isStaff: true,
lastName: "User", lastName: "User",
userPermissions: [ userPermissions: [
{ {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import { InputProps } from "@material-ui/core/Input"; import { InputProps } from "@material-ui/core/Input";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import { ExtendedFormHelperTextProps } from "@saleor/channels/components/ChannelForm/types";
import { FetchMoreProps } from "@saleor/types"; import { FetchMoreProps } from "@saleor/types";
import classNames from "classnames"; import classNames from "classnames";
import Downshift, { ControllerStateAndHelpers } from "downshift"; import Downshift, { ControllerStateAndHelpers } from "downshift";
@ -42,6 +43,7 @@ export interface SingleAutocompleteSelectFieldProps
InputProps?: InputProps; InputProps?: InputProps;
fetchChoices?: (value: string) => void; fetchChoices?: (value: string) => void;
onChange: (event: React.ChangeEvent<any>) => void; onChange: (event: React.ChangeEvent<any>) => void;
FormHelperTextProps?: ExtendedFormHelperTextProps;
} }
const DebounceAutocomplete: React.ComponentType<DebounceProps< const DebounceAutocomplete: React.ComponentType<DebounceProps<
@ -69,6 +71,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
fetchChoices, fetchChoices,
onChange, onChange,
onFetchMore, onFetchMore,
FormHelperTextProps,
...rest ...rest
} = props; } = props;
const classes = useStyles(props); const classes = useStyles(props);
@ -178,6 +181,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
error={error} error={error}
disabled={disabled} disabled={disabled}
helperText={helperText} helperText={helperText}
FormHelperTextProps={FormHelperTextProps}
label={label} label={label}
fullWidth={true} 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 { return {
alertDot: {
"&:before": { backgroundColor: yellow[500], ...dot }
},
errorDot: { errorDot: {
"&:before": { backgroundColor: theme.palette.error.main, ...dot } "&:before": { backgroundColor: theme.palette.error.main, ...dot }
}, },
neutralDot: { neutralDot: {
"&:before": { backgroundColor: yellow[500], ...dot } "&:before": { backgroundColor: grey[300], ...dot }
}, },
root: { root: {
display: "inline-block", display: "inline-block",
@ -35,9 +38,6 @@ const useStyles = makeStyles(
}, },
successDot: { successDot: {
"&:before": { backgroundColor: theme.palette.primary.main, ...dot } "&:before": { backgroundColor: theme.palette.primary.main, ...dot }
},
unspecifiedDot: {
"&:before": { backgroundColor: grey[500], ...dot }
} }
}; };
}, },
@ -47,7 +47,7 @@ const useStyles = makeStyles(
interface StatusLabelProps { interface StatusLabelProps {
className?: string; className?: string;
label: string | React.ReactNode; label: string | React.ReactNode;
status: "success" | "neutral" | "unspecified" | "error" | string; status: "success" | "alert" | "neutral" | "error" | string;
typographyProps?: TypographyProps; typographyProps?: TypographyProps;
} }
@ -62,8 +62,8 @@ const StatusLabel: React.FC<StatusLabelProps> = props => {
[classes.root]: true, [classes.root]: true,
[className]: true, [className]: true,
[classes.successDot]: status === "success", [classes.successDot]: status === "success",
[classes.alertDot]: status === "alert",
[classes.neutralDot]: status === "neutral", [classes.neutralDot]: status === "neutral",
[classes.unspecifiedDot]: status === "unspecified",
[classes.errorDot]: status === "error" [classes.errorDot]: status === "error"
})} })}
> >

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