Merge branch 'master' into SALEOR-1738-Tests-for-home-page
This commit is contained in:
commit
68eb5309ef
330 changed files with 14422 additions and 6283 deletions
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -21,10 +21,12 @@ greatly reduce the amount of work needed to review your work. -->
|
|||
1. [ ] Data-test are added for new elements.
|
||||
1. [ ] Type definitions are up to date.
|
||||
1. [ ] Changes are mentioned in the changelog.
|
||||
1. [ ] The changes are tested in different browsers (Chrome, Firefox, Safari).
|
||||
1. [ ] The changes are tested in light and dark mode.
|
||||
|
||||
### Test environment config
|
||||
|
||||
<!-- Do not remove this section. It is required to properly setup test instance.
|
||||
Modify API_URI if you want test instance to use custom backend. -->
|
||||
|
||||
API_URI=https://master.staging.saleor.rocks/graphql/
|
||||
API_URI=https://master.staging.saleor.cloud/graphql/
|
||||
|
|
7
.github/workflows/deploy-staging.yaml
vendored
7
.github/workflows/deploy-staging.yaml
vendored
|
@ -11,6 +11,12 @@ jobs:
|
|||
API_URI: https://master.staging.saleor.cloud/graphql/
|
||||
APP_MOUNT_URI: /dashboard/
|
||||
STATIC_URL: /dashboard/static/
|
||||
SENTRY_ORG: saleor
|
||||
SENTRY_PROJECT: dashboard
|
||||
SENTRY_URL_PREFIX: "~/dashboard/static"
|
||||
ENVIRONMENT: master-staging
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Package
|
||||
|
@ -41,4 +47,3 @@ jobs:
|
|||
aws s3 sync build/dashboard s3://${{ secrets.AWS_STAGING_DEPLOYMENT_BUCKET }}/saleor-master-staging/static/
|
||||
aws s3 cp build/dashboard/index.html s3://${{ secrets.AWS_STAGING_DEPLOYMENT_BUCKET }}/saleor-master-staging/
|
||||
aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_STAGING_CF_DIST_ID }} --paths "/dashboard*"
|
||||
|
||||
|
|
4
.github/workflows/test-env-deploy.yml
vendored
4
.github/workflows/test-env-deploy.yml
vendored
|
@ -59,7 +59,7 @@ jobs:
|
|||
- name: Run build
|
||||
env:
|
||||
# Use custom API_URI or the default one
|
||||
API_URI: ${{ steps.api_uri.outputs.custom_api_uri || 'https://master.staging.saleor.rocks/graphql/' }}
|
||||
API_URI: ${{ steps.api_uri.outputs.custom_api_uri || 'https://master.staging.saleor.cloud/graphql/' }}
|
||||
APP_MOUNT_URI: /
|
||||
STATIC_URL: /
|
||||
run: |
|
||||
|
@ -88,7 +88,7 @@ jobs:
|
|||
aws s3 sync ./build/storybook s3://${{ secrets.AWS_TEST_DEPLOYMENT_BUCKET }}/${{ steps.set-domain.outputs.domain }}/storybook
|
||||
|
||||
- name: Invalidate cache
|
||||
run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_TEST_CF_DIST_ID }} --paths "/${{ env.domain }}/*"
|
||||
run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_TEST_CF_DIST_ID }} --paths "/${{ steps.set-domain.outputs.domain }}/*"
|
||||
|
||||
- name: Update deployment status
|
||||
uses: bobheadxi/deployments@v0.4.2
|
||||
|
|
|
@ -9,9 +9,16 @@ All notable, unreleased changes to this project will be documented in this file.
|
|||
- Add shipping methods to translation section - #864 by @marekchoinski
|
||||
- New Miscellaneous and Product refunds - #870 by @orzechdev
|
||||
- Add zip code exclusion - #877 by @dominik-zeglen
|
||||
- Add order reissue
|
||||
- Update quantity column in Inventory part of Product Variant view - #904 by @dominik-zeglen
|
||||
- Add file attributes - #884 by @orzechdev
|
||||
- Add shipping delivery days - #914 by @orzechdev
|
||||
- Guard against non-staff users logging in - #947 by @jwm0
|
||||
- Add reference attributes - #917 by @orzechdev
|
||||
- Add product reference attributes - #948 by @orzechdev
|
||||
- Drop descriptionJson and contentJson fields - #950 by @jwm0
|
||||
- Add error tracking with Sentry adapter - #956 by @jwm0
|
||||
- Add OAuth2 login with OpenID support - #963 by @orzechdev
|
||||
|
||||
# 2.11.1
|
||||
|
||||
|
|
41
README.md
41
README.md
|
@ -61,7 +61,7 @@ $ npm i
|
|||
|
||||
### Configuration
|
||||
|
||||
There are two environment variables available for configuration:
|
||||
There following environment variables are available for configuration:
|
||||
|
||||
- `API_URI` (required) - URI of a running instance of Saleor GraphQL API.
|
||||
If you are running Saleor locally with the default settings, set `API_URI` to: `http://localhost:8000/graphql/`.
|
||||
|
@ -88,4 +88,43 @@ To build the application bundle run:
|
|||
$ npm run build
|
||||
```
|
||||
|
||||
### Error Tracking
|
||||
|
||||
Saleor Dashboard is using a generic error tracking wrapper function that takes care of the most popular use cases:
|
||||
|
||||
- initializing the tracker
|
||||
- capturing exceptions and (optionally) displaying the event id
|
||||
- setting basic user data (this is opt-in and disabled by default)
|
||||
|
||||
By default it ships with a Sentry adapter but any kind of error tracking software can be used by creating a custom adapter (using Sentry and TS types as an example).
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
// src/services/errorTracking/index.ts
|
||||
|
||||
import { CustomAdapter } from "./adapters/";
|
||||
|
||||
const errorTracker = ErrorTrackerFactory(CustomAdapter(config));
|
||||
```
|
||||
|
||||
##### Usage with Sentry adapter:
|
||||
|
||||
Sentry is used as the default tracker so no changes in code are necessary and the configuration is done via environment variables.
|
||||
|
||||
The following environment variables are available:
|
||||
|
||||
```
|
||||
# Required
|
||||
SENTRY_DSN=
|
||||
|
||||
# Optional
|
||||
# https://docs.sentry.io/product/cli/configuration/
|
||||
SENTRY_AUTH_TOKEN=
|
||||
SENTRY_ORG=
|
||||
SENTRY_PROJECT=
|
||||
SENTRY_URL_PREFIX=
|
||||
ENVIRONMENT=
|
||||
```
|
||||
|
||||
#### Crafted with ❤️ by [Mirumee Software](https://mirumee.com)
|
||||
|
|
|
@ -3,6 +3,14 @@
|
|||
"context": "dialog header",
|
||||
"string": "Cancel Order"
|
||||
},
|
||||
"amount title": {
|
||||
"context": "amount title",
|
||||
"string": "Refunded amount"
|
||||
},
|
||||
"by preposition": {
|
||||
"context": "by preposition",
|
||||
"string": "by"
|
||||
},
|
||||
"configurationMenuAttributes": {
|
||||
"string": "Determine attributes used to create product types"
|
||||
},
|
||||
|
@ -42,6 +50,42 @@
|
|||
"configurationPluginsPages": {
|
||||
"string": "View and update your plugins and their settings."
|
||||
},
|
||||
"event products list title refunded": {
|
||||
"context": "refunded products list title",
|
||||
"string": "Products refunded"
|
||||
},
|
||||
"event products list title replaced": {
|
||||
"context": "replaced products list title",
|
||||
"string": "Products replaced"
|
||||
},
|
||||
"event products list title returned": {
|
||||
"context": "returned products list title",
|
||||
"string": "Products returned"
|
||||
},
|
||||
"event products title draft reissued": {
|
||||
"context": "draft created from replace products list title",
|
||||
"string": "Products replaced"
|
||||
},
|
||||
"event title draft reissued": {
|
||||
"context": "draft created from replace event title",
|
||||
"string": "Draft was reissued from order "
|
||||
},
|
||||
"event title marked as paid": {
|
||||
"context": "order marked as paid event title",
|
||||
"string": "Order was marked as paid by"
|
||||
},
|
||||
"event title refunded": {
|
||||
"context": "refunded event title",
|
||||
"string": "Products were refunded by "
|
||||
},
|
||||
"event title replaced": {
|
||||
"context": "replaced event title",
|
||||
"string": "Products were replaced by "
|
||||
},
|
||||
"event title returned": {
|
||||
"context": "returned event title",
|
||||
"string": "Products were returned by"
|
||||
},
|
||||
"homeActivityCardHeader": {
|
||||
"context": "header",
|
||||
"string": "Activity"
|
||||
|
@ -336,6 +380,10 @@
|
|||
"context": "unassign product from sale, button",
|
||||
"string": "Unassign"
|
||||
},
|
||||
"shipment refund title": {
|
||||
"context": "shipment refund title",
|
||||
"string": "Shipment was refunded"
|
||||
},
|
||||
"shippingZoneDetailsDialogsDeleteShippingMethod": {
|
||||
"context": "delete shipping method",
|
||||
"string": "Are you sure you want to delete {name}?"
|
||||
|
@ -733,37 +781,53 @@
|
|||
"context": "dialog content",
|
||||
"string": "Are you sure you want to delete {attributeName}?"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_1005562666": {
|
||||
"context": "attribute's editor component",
|
||||
"string": "Catalog Input type for Store Owner"
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_attributeLabel": {
|
||||
"context": "attribute's label",
|
||||
"string": "Default Label"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_1336738461": {
|
||||
"context": "product attribute type",
|
||||
"string": "Dropdown"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_1376373679": {
|
||||
"context": "file attribute type",
|
||||
"string": "File"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_2592224946": {
|
||||
"context": "check to require attribute to have value",
|
||||
"string": "Value Required"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_3334509011": {
|
||||
"context": "product attribute type",
|
||||
"string": "Multiple Select"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_3605174225": {
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_attributeSlug": {
|
||||
"context": "attribute's slug short code label",
|
||||
"string": "Attribute Code"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_4107478955": {
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_attributeSlugHelperText": {
|
||||
"context": "attribute slug input field helper text",
|
||||
"string": "This is used internally. Make sure you don’t use spaces"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_691600601": {
|
||||
"context": "attribute's label",
|
||||
"string": "Default Label"
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_dropdown": {
|
||||
"context": "product attribute type",
|
||||
"string": "Dropdown"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_entityType": {
|
||||
"context": "attribute's editor component entity",
|
||||
"string": "Entity"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_file": {
|
||||
"context": "file attribute type",
|
||||
"string": "File"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_inputType": {
|
||||
"context": "attribute's editor component",
|
||||
"string": "Catalog Input type for Store Owner"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_multiselect": {
|
||||
"context": "product attribute type",
|
||||
"string": "Multiple Select"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_page": {
|
||||
"context": "page attribute entity type",
|
||||
"string": "Pages"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_product": {
|
||||
"context": "product attribute entity type",
|
||||
"string": "Products"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_references": {
|
||||
"context": "references attribute type",
|
||||
"string": "References"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeDetails_dot_valueRequired": {
|
||||
"context": "check to require attribute to have value",
|
||||
"string": "Value Required"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeListPage_dot_2417065806": {
|
||||
"context": "tab name",
|
||||
|
@ -844,43 +908,46 @@
|
|||
"context": "page title",
|
||||
"string": "Create New Attribute"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_1318123158": {
|
||||
"context": "attribute is filterable in storefront",
|
||||
"string": "Use in Faceted Navigation"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_1877630205": {
|
||||
"context": "attribute properties regarding storefront",
|
||||
"string": "Storefront Properties"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_26409543": {
|
||||
"context": "attribute properties regarding dashboard",
|
||||
"string": "Dashboard Properties"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_3135366329": {
|
||||
"context": "attribute visibility in storefront",
|
||||
"string": "Public"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_3590282519": {
|
||||
"context": "attribute position in storefront filters",
|
||||
"string": "Position in faceted navigation"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_3758203740": {
|
||||
"string": "If enabled, attribute will be accessible to customers."
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_4048785456": {
|
||||
"string": "If enabled this attribute can be used as a column in product table."
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_673770329": {
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_availableInGrid": {
|
||||
"context": "add attribute as column in product list table",
|
||||
"string": "Add to Column Options"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_714335445": {
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_availableInGridCaption": {
|
||||
"context": "caption",
|
||||
"string": "If enabled this attribute can be used as a column in product table."
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_dashboardPropertiesTitle": {
|
||||
"context": "attribute properties regarding dashboard",
|
||||
"string": "Dashboard Properties"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_filterableInDashboard": {
|
||||
"context": "use attribute in filtering",
|
||||
"string": "Use in Filtering"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_787251583": {
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_filterableInDashboardCaption": {
|
||||
"context": "caption",
|
||||
"string": "If enabled, you’ll be able to use this attribute to filter products in product list."
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_filterableInStorefront": {
|
||||
"context": "attribute is filterable in storefront",
|
||||
"string": "Use in Faceted Navigation"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_storefrontPropertiesTitle": {
|
||||
"context": "attribute properties regarding storefront",
|
||||
"string": "Storefront Properties"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_storefrontSearchPosition": {
|
||||
"context": "attribute position in storefront filters",
|
||||
"string": "Position in faceted navigation"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_visibleInStorefront": {
|
||||
"context": "attribute visibility in storefront",
|
||||
"string": "Public"
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeProperties_dot_visibleInStorefrontCaption": {
|
||||
"context": "caption",
|
||||
"string": "If enabled, attribute will be accessible to customers."
|
||||
},
|
||||
"src_dot_attributes_dot_components_dot_AttributeValueDeleteDialog_dot_1326420604": {
|
||||
"context": "delete attribute value",
|
||||
"string": "Are you sure you want to delete \"{name}\" value?"
|
||||
|
@ -949,12 +1016,23 @@
|
|||
"src_dot_auth_dot_components_dot_LoginPage_dot_2237029987": {
|
||||
"string": "Password"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_2981302356": {
|
||||
"context": "link",
|
||||
"string": "Use this link to recover it"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_3476994590": {
|
||||
"string": "Sorry, your username and/or password are incorrect. Please try again."
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_4028609483": {
|
||||
"context": "button",
|
||||
"string": "Reset your password"
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_3762459576": {
|
||||
"context": "description",
|
||||
"string": "or login using"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_534894384": {
|
||||
"string": "Sorry, login went wrong. Please try again."
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_LoginPage_dot_599516345": {
|
||||
"context": "description",
|
||||
"string": "Forgot password? {resetPasswordLink}"
|
||||
},
|
||||
"src_dot_auth_dot_components_dot_NewPasswordPage_dot_1254879564": {
|
||||
"string": "New Password"
|
||||
|
@ -1502,6 +1580,18 @@
|
|||
"src_dot_components_dot_AssignAttributeDialog_dot_902296540": {
|
||||
"string": "Search Attributes"
|
||||
},
|
||||
"src_dot_components_dot_AssignAttributeValueDialog_dot_header": {
|
||||
"context": "dialog header",
|
||||
"string": "Assign Attribute Value"
|
||||
},
|
||||
"src_dot_components_dot_AssignAttributeValueDialog_dot_searchLabel": {
|
||||
"context": "label",
|
||||
"string": "Search Attribute Value"
|
||||
},
|
||||
"src_dot_components_dot_AssignAttributeValueDialog_dot_searchPlaceholder": {
|
||||
"context": "placeholder",
|
||||
"string": "Search by value name, etc..."
|
||||
},
|
||||
"src_dot_components_dot_AssignCategoryDialog_dot_3125506097": {
|
||||
"context": "dialog header",
|
||||
"string": "Assign Category"
|
||||
|
@ -1543,6 +1633,10 @@
|
|||
"src_dot_components_dot_AttributeUnassignDialog_dot_2037985699": {
|
||||
"string": "Are you sure you want to unassign {attributeName} from {itemTypeName}?"
|
||||
},
|
||||
"src_dot_components_dot_Attributes_dot_3824528779": {
|
||||
"context": "button label",
|
||||
"string": "Assign references"
|
||||
},
|
||||
"src_dot_components_dot_Attributes_dot_attributesNumber": {
|
||||
"context": "number of attributes",
|
||||
"string": "{number} Attributes"
|
||||
|
@ -3032,6 +3126,9 @@
|
|||
"src_dot_hooks_dot_3382262667": {
|
||||
"string": "Variant {name} has been set as default."
|
||||
},
|
||||
"src_dot_insufficientPermissions": {
|
||||
"string": "Insufficient permissions"
|
||||
},
|
||||
"src_dot_lastName": {
|
||||
"string": "Last Name"
|
||||
},
|
||||
|
@ -3130,13 +3227,17 @@
|
|||
"src_dot_orders_dot_components_dot_OrderCustomer_dot_4282475982": {
|
||||
"string": "Billing Address"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_1854613983": {
|
||||
"context": "button",
|
||||
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_cancelOrder": {
|
||||
"context": "cancel button",
|
||||
"string": "Cancel order"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_3086420445": {
|
||||
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_confirmOrder": {
|
||||
"context": "save button",
|
||||
"string": "confirm order"
|
||||
"string": "Confirm order"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderDetailsPage_dot_returnOrder": {
|
||||
"context": "return button",
|
||||
"string": "Return / Replace order"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderDraftCancelDialog_dot_1961675716": {
|
||||
"context": "dialog header",
|
||||
|
@ -3272,6 +3373,37 @@
|
|||
"context": "product's sku",
|
||||
"string": "SKU"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_1119771899": {
|
||||
"context": "add tracking button",
|
||||
"string": "Add tracking"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_2211099657": {
|
||||
"context": "edit tracking button",
|
||||
"string": "Edit tracking"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_2845258362": {
|
||||
"context": "refund button",
|
||||
"string": "Refund"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_3254150098": {
|
||||
"string": "Tracking Number: {trackingNumber}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_732594284": {
|
||||
"context": "button",
|
||||
"string": "Cancel Fulfillment"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_fulfilled": {
|
||||
"context": "fulfillment group",
|
||||
"string": "Fulfilled from: "
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_restocked": {
|
||||
"context": "restocked group",
|
||||
"string": "Restocked from: "
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfilledProductsCard_dot_tracking": {
|
||||
"context": "tracking number",
|
||||
"string": "Tracking Number: {trackingNumber}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillmentCancelDialog_dot_1097287358": {
|
||||
"string": "Are you sure you want to cancel fulfillment? Canceling a fulfillment will restock products at a selected warehouse."
|
||||
},
|
||||
|
@ -3309,71 +3441,10 @@
|
|||
"context": "dialog header",
|
||||
"string": "Add Tracking Code"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_1119771899": {
|
||||
"context": "fulfillment group tracking number",
|
||||
"string": "Add tracking"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_1134347598": {
|
||||
"context": "product price",
|
||||
"string": "Price"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_1895667608": {
|
||||
"context": "product name",
|
||||
"string": "Product"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_2211099657": {
|
||||
"context": "fulfillment group tracking number",
|
||||
"string": "Edit tracking"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_2567258278": {
|
||||
"context": "refunded fulfillment, section header",
|
||||
"string": "Refunded ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_2796503714": {
|
||||
"context": "ordered product quantity",
|
||||
"string": "Quantity"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_3254150098": {
|
||||
"string": "Tracking Number: {trackingNumber}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_3494686506": {
|
||||
"context": "section header",
|
||||
"string": "Fulfilled ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_4039425374": {
|
||||
"context": "cancelled fulfillment, section header",
|
||||
"string": "Cancelled ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_693960049": {
|
||||
"context": "ordered product sku",
|
||||
"string": "SKU"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_732594284": {
|
||||
"context": "button",
|
||||
"string": "Cancel Fulfillment"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_77179533": {
|
||||
"context": "fulfillment group",
|
||||
"string": "Fulfilled from: {warehouseName}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderFulfillment_dot_878013594": {
|
||||
"context": "order line total price",
|
||||
"string": "Total"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderHistory_dot_1154330234": {
|
||||
"context": "transaction reference",
|
||||
"string": "Transaction Reference {transactionReference}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderHistory_dot_1230178536": {
|
||||
"context": "order history message",
|
||||
"string": "Order address was updated"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderHistory_dot_123236698": {
|
||||
"string": "Shipment was refunded"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderHistory_dot_1322321687": {
|
||||
"string": "Refunded amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderHistory_dot_1463685940": {
|
||||
"context": "order history message",
|
||||
"string": "Order was marked as paid"
|
||||
|
@ -3489,9 +3560,6 @@
|
|||
"context": "order history message",
|
||||
"string": "Payment failed"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderHistory_dot_492197448": {
|
||||
"string": "Products refunded"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderHistory_dot_493321552": {
|
||||
"context": "order history message",
|
||||
"string": "Order cancel information was sent to customer"
|
||||
|
@ -3512,6 +3580,14 @@
|
|||
"context": "order history message",
|
||||
"string": "Order was cancelled"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderHistory_dot_description": {
|
||||
"context": "replacement created order history message description",
|
||||
"string": "was created for replaced products"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderHistory_dot_draftNumber": {
|
||||
"context": "replacement created order history message draft number",
|
||||
"string": "Draft #{orderNumber} "
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderInvoiceEmailSendDialog_dot_1821123638": {
|
||||
"string": "Are you sure you want to send this invoice: {invoiceNumber} to the customer?"
|
||||
},
|
||||
|
@ -3695,71 +3771,29 @@
|
|||
"src_dot_orders_dot_components_dot_OrderProductAddDialog_dot_353369701": {
|
||||
"string": "No products matching given query"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1580639738": {
|
||||
"context": "order refund amount",
|
||||
"string": "Proposed refund amount"
|
||||
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_1134347598": {
|
||||
"context": "product price",
|
||||
"string": "Price"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1705174606": {
|
||||
"context": "order refund amount",
|
||||
"string": "Max Refund"
|
||||
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_1895667608": {
|
||||
"context": "product name",
|
||||
"string": "Product"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_1734445951": {
|
||||
"context": "order refund amount",
|
||||
"string": "Refund total amount"
|
||||
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_2796503714": {
|
||||
"context": "ordered product quantity",
|
||||
"string": "Quantity"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2045860028": {
|
||||
"context": "order refund amount",
|
||||
"string": "Authorized Amount"
|
||||
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_693960049": {
|
||||
"context": "ordered product sku",
|
||||
"string": "SKU"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2854815744": {
|
||||
"context": "order refund amount",
|
||||
"string": "Previously refunded"
|
||||
"src_dot_orders_dot_components_dot_OrderProductsCardElements_dot_878013594": {
|
||||
"context": "order line total price",
|
||||
"string": "Total"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_2907874606": {
|
||||
"context": "order refund amount",
|
||||
"string": "Selected products value"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmountValues_dot_79173946": {
|
||||
"context": "order refund amount",
|
||||
"string": "Shipment cost"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_120912052": {
|
||||
"context": "order refund amount",
|
||||
"string": "Refunded items can’t be fulfilled"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_159210811": {
|
||||
"string": "Amount must be bigger than 0"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_2256869831": {
|
||||
"context": "section header",
|
||||
"string": "Refunded Amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_2845258362": {
|
||||
"context": "order refund amount, input button",
|
||||
"string": "Refund"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_4033685232": {
|
||||
"string": "Amount cannot be bigger than max refund"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_40513382": {
|
||||
"context": "order refund amount, input button",
|
||||
"string": "Refund {currency} {amount}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_4224226791": {
|
||||
"context": "label",
|
||||
"string": "Automatic Amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_508357513": {
|
||||
"context": "label",
|
||||
"string": "Manual Amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_553737700": {
|
||||
"context": "checkbox",
|
||||
"string": "Refund shipment costs"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundAmount_dot_75546233": {
|
||||
"context": "order refund amount, input label",
|
||||
"string": "Amount"
|
||||
"src_dot_orders_dot_components_dot_OrderRefundFulfilledProducts_dot_1097582574": {
|
||||
"context": "section header returned",
|
||||
"string": "Fulfillment returned"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundFulfilledProducts_dot_1134347598": {
|
||||
"context": "tabel column header",
|
||||
|
@ -3804,6 +3838,90 @@
|
|||
"context": "page header with order number",
|
||||
"string": "Order #{orderNumber}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_2256869831": {
|
||||
"context": "section header",
|
||||
"string": "Refunded Amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_40513382": {
|
||||
"context": "order refund amount, input button",
|
||||
"string": "Refund {currency} {amount}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_4224226791": {
|
||||
"context": "label",
|
||||
"string": "Automatic Amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_508357513": {
|
||||
"context": "label",
|
||||
"string": "Manual Amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_553737700": {
|
||||
"context": "checkbox",
|
||||
"string": "Refund shipment costs"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_982301568": {
|
||||
"context": "label",
|
||||
"string": "No refund"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_amountTooBig": {
|
||||
"context": "Amount error message",
|
||||
"string": "Amount cannot be bigger than max refund"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_amountTooSmall": {
|
||||
"context": "Amount error message",
|
||||
"string": "Amount must be bigger than 0"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_authorizedAmount": {
|
||||
"context": "order refund amount",
|
||||
"string": "Authorized Amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_label": {
|
||||
"context": "order refund amount, input label",
|
||||
"string": "Amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_maxRefund": {
|
||||
"context": "order refund amount",
|
||||
"string": "Max Refund"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_previouslyRefunded": {
|
||||
"context": "order refund amount",
|
||||
"string": "Previously refunded"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_proposedRefundAmount": {
|
||||
"context": "order refund amount",
|
||||
"string": "Proposed refund amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_refundButton": {
|
||||
"context": "order refund amount button",
|
||||
"string": "Refund"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_refundCannotBeFulfilled": {
|
||||
"context": "order refund subtitle",
|
||||
"string": "Refunded items can't be fulfilled"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_refundTotalAmount": {
|
||||
"context": "order refund amount",
|
||||
"string": "Refund total amount"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_replacedProductsValue": {
|
||||
"context": "order refund amount",
|
||||
"string": "Replaced Products Value"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_returnButton": {
|
||||
"context": "order return amount button",
|
||||
"string": "Return & Replace products"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_returnCannotBeFulfilled": {
|
||||
"context": "order return subtitle",
|
||||
"string": "Returned items can't be fulfilled"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_selectedProductsValue": {
|
||||
"context": "order refund amount",
|
||||
"string": "Selected Products Value"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundReturnAmount_dot_shipmentCost": {
|
||||
"context": "order refund amount",
|
||||
"string": "Shipment Cost"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderRefundUnfulfilledProducts_dot_1134347598": {
|
||||
"context": "tabel column header",
|
||||
"string": "Price"
|
||||
|
@ -3851,6 +3969,82 @@
|
|||
"context": "refund type",
|
||||
"string": "Refund Products"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_1134347598": {
|
||||
"context": "table column header",
|
||||
"string": "Price"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_1784788864": {
|
||||
"context": "table column header",
|
||||
"string": "Return"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_1895667608": {
|
||||
"context": "table column header",
|
||||
"string": "Product"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_2049070632": {
|
||||
"context": "table column header",
|
||||
"string": "Replace"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_3988345170": {
|
||||
"context": "button",
|
||||
"string": "Set maximal quantities"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_cancelled": {
|
||||
"context": "cancelled fulfillment, section header",
|
||||
"string": "Cancelled ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_description": {
|
||||
"context": "product no longer exists error description",
|
||||
"string": "This product is no longer in database so it can’t be replaced, nor returned"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_fulfilled": {
|
||||
"context": "section header",
|
||||
"string": "Fulfilled ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_improperValue": {
|
||||
"context": "error message",
|
||||
"string": "Improper value"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_refunded": {
|
||||
"context": "refunded fulfillment, section header",
|
||||
"string": "Refunded ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_refundedAndReturned": {
|
||||
"context": "cancelled fulfillment, section header",
|
||||
"string": "Refunded and Returned ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_replaced": {
|
||||
"context": "refunded fulfillment, section header",
|
||||
"string": "Replaced ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_returned": {
|
||||
"context": "refunded fulfillment, section header",
|
||||
"string": "Returned ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_title": {
|
||||
"context": "product no longer exists error title",
|
||||
"string": "Product no longer exists"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_titleFulfilled": {
|
||||
"context": "section header",
|
||||
"string": "Fulfillment - #{fulfilmentId}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_titleUnfulfilled": {
|
||||
"context": "section header",
|
||||
"string": "Unfulfilled Items"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_OrderReturnRefundItemsCard_dot_unfulfilled": {
|
||||
"context": "section header",
|
||||
"string": "Unfulfilled"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_appTitle": {
|
||||
"context": "page header with order number",
|
||||
"string": "Order #{orderNumber}"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderReturnPage_dot_pageTitle": {
|
||||
"context": "page header",
|
||||
"string": "Order no. {orderNumber} - Replace/Return"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderSettingsPage_dot_1149215359": {
|
||||
"context": "header",
|
||||
"string": "Order settings"
|
||||
|
@ -3874,34 +4068,10 @@
|
|||
"context": "dialog header",
|
||||
"string": "Edit Shipping Method"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_1134347598": {
|
||||
"context": "product unit price",
|
||||
"string": "Price"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_1895667608": {
|
||||
"context": "product name",
|
||||
"string": "Product"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_2095687440": {
|
||||
"src_dot_orders_dot_components_dot_OrderUnfulfilledProductsCard_dot_2095687440": {
|
||||
"context": "button",
|
||||
"string": "Fulfill"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_2796503714": {
|
||||
"context": "ordered products",
|
||||
"string": "Quantity"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_2886647373": {
|
||||
"context": "section header",
|
||||
"string": "Unfulfilled ({quantity})"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_693960049": {
|
||||
"context": "ordered product sku",
|
||||
"string": "SKU"
|
||||
},
|
||||
"src_dot_orders_dot_components_dot_OrderUnfulfilledItems_dot_878013594": {
|
||||
"context": "order line total price",
|
||||
"string": "Total"
|
||||
},
|
||||
"src_dot_orders_dot_views_dot_OrderDetails_dot_1039259580": {
|
||||
"string": "We’re generating the invoice you requested. Please wait a couple of moments"
|
||||
},
|
||||
|
@ -3991,6 +4161,18 @@
|
|||
"context": "order refunded success message",
|
||||
"string": "Refunded Items"
|
||||
},
|
||||
"src_dot_orders_dot_views_dot_OrderReturn_dot_cannotRefundDescription": {
|
||||
"context": "order return error description when cannot refund",
|
||||
"string": "We’ve encountered a problem while refunding the products. Product’s were not refunded. Please try again."
|
||||
},
|
||||
"src_dot_orders_dot_views_dot_OrderReturn_dot_cannotRefundTitle": {
|
||||
"context": "order return error title when cannot refund",
|
||||
"string": "Couldn't refund products"
|
||||
},
|
||||
"src_dot_orders_dot_views_dot_OrderReturn_dot_successAlert": {
|
||||
"context": "order returned success message",
|
||||
"string": "Successfully returned products!"
|
||||
},
|
||||
"src_dot_pageTypes": {
|
||||
"context": "page types section name",
|
||||
"string": "Page Types"
|
||||
|
@ -4237,6 +4419,10 @@
|
|||
"context": "payment status",
|
||||
"string": "Partially refunded"
|
||||
},
|
||||
"src_dot_partiallyReturned": {
|
||||
"context": "order status",
|
||||
"string": "Partially returned"
|
||||
},
|
||||
"src_dot_permissionGroups": {
|
||||
"context": "permission groups section name",
|
||||
"string": "Permission Groups"
|
||||
|
@ -5272,6 +5458,10 @@
|
|||
"src_dot_requiredField": {
|
||||
"string": "This field is required"
|
||||
},
|
||||
"src_dot_returned": {
|
||||
"context": "order status",
|
||||
"string": "Returned"
|
||||
},
|
||||
"src_dot_sales": {
|
||||
"context": "sales section name",
|
||||
"string": "Sales"
|
||||
|
@ -6251,6 +6441,9 @@
|
|||
"src_dot_translations_dot_components_dot_TranslationsVouchersPage_dot_2599922713": {
|
||||
"string": "Voucher Name"
|
||||
},
|
||||
"src_dot_unauthorizedDashboardAccess": {
|
||||
"string": "Only staff users can access the dashboard"
|
||||
},
|
||||
"src_dot_unconfirmed": {
|
||||
"context": "order status",
|
||||
"string": "Unconfirmed"
|
||||
|
@ -6707,6 +6900,10 @@
|
|||
"context": "table column header",
|
||||
"string": "Quantity"
|
||||
},
|
||||
"transaction reference subtitle": {
|
||||
"context": "transaction reference subtitle",
|
||||
"string": "Transaction reference"
|
||||
},
|
||||
"voucherDetailsUnassignCategory": {
|
||||
"context": "unassign category from voucher, button",
|
||||
"string": "Unassign"
|
||||
|
|
132
package-lock.json
generated
132
package-lock.json
generated
|
@ -30069,9 +30069,9 @@
|
|||
}
|
||||
},
|
||||
"@editorjs/editorjs": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.19.0.tgz",
|
||||
"integrity": "sha512-8PUVaBZx69IrG8dNrE+FZbHSiRTR8ql8L/cmEi1mOdEdTqnOLq5Wv9dgemK00mBWEgNoavMAjtGQpItGknAa8A==",
|
||||
"version": "2.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.19.1.tgz",
|
||||
"integrity": "sha512-5lN7r5B2NCE8VJdsS3poX3Qg9rNwzpxZ+6Jjif3hAVZTYpQwg5wXEpAHFNbuavS0T5Ji+0ID31DQFotVI4PosA==",
|
||||
"requires": {
|
||||
"codex-notifier": "^1.1.2",
|
||||
"codex-tooltip": "^1.0.1"
|
||||
|
@ -31110,6 +31110,120 @@
|
|||
"any-observable": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"@sentry/browser": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.0.0.tgz",
|
||||
"integrity": "sha512-R4+MHb5FyVZCz3EVnaquvT1mwOM2MWP4gBqjYEADY5m0XWoHiJf0skFkWt8iEKJanzGbhl4PMb9gHuJj6YfVLw==",
|
||||
"requires": {
|
||||
"@sentry/core": "6.0.0",
|
||||
"@sentry/types": "6.0.0",
|
||||
"@sentry/utils": "6.0.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/cli": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.61.0.tgz",
|
||||
"integrity": "sha512-pHEhqP1bB4sdO7N5ow/IkRBrPbKT9HZRinq4PhTVIvmG+NW4VVuVZ6k4tlbp+JXmzMcUc/iXynVkTL7zJIlTQw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"mkdirp": "^0.5.5",
|
||||
"node-fetch": "^2.6.0",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"https-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
|
||||
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.0.0.tgz",
|
||||
"integrity": "sha512-afAiOachs/WfGWc9LsJBFnJMhqQVENyzfSMnf7sLRvxPAw8n7IrXY0R09MKmG0SlAnTKN2pWoQFzFF+J3NuHBA==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.0.0",
|
||||
"@sentry/minimal": "6.0.0",
|
||||
"@sentry/types": "6.0.0",
|
||||
"@sentry/utils": "6.0.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.0.0.tgz",
|
||||
"integrity": "sha512-s8IsW6LvEH7ACnniQcxxb/9uEyjmoQ/TAoryTJN2qyPzzrHTw8NCyMuJvK+8ivUvRViz5AvtuOFf8AJlh9lzeA==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.0.0",
|
||||
"@sentry/utils": "6.0.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.0.0.tgz",
|
||||
"integrity": "sha512-daYdEzTr+ERMwViu6RpWHOfk0oZrSNqdx+7bejTqmFHqO4pt+9ZrMiw3vinL+MWQcKXwD95uXBz6O/ryrVdPtg==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.0.0",
|
||||
"@sentry/types": "6.0.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/react": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-6.0.0.tgz",
|
||||
"integrity": "sha512-GYX110NSodd8wGUbnyxemndTijM+U7dI/WjFSPOyJdLB2hzzPjJ9kUqtuobT/JlGzbWE2278WysAuySne6bUGw==",
|
||||
"requires": {
|
||||
"@sentry/browser": "6.0.0",
|
||||
"@sentry/minimal": "6.0.0",
|
||||
"@sentry/types": "6.0.0",
|
||||
"@sentry/utils": "6.0.0",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.0.0.tgz",
|
||||
"integrity": "sha512-yueRSRGPCahuju/UMdtOt8LIIncbpwLINQd9Q8E4OXtoPpMHR6Oun8sMKCPd+Wq3piI5yRDzKkGCl+sH7mHVrA=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.0.0.tgz",
|
||||
"integrity": "sha512-dMMWOT69bQ4CF1R33dOnXIOyiHRWsUAON3nFVljV1JNNTDA69YwaF9f5FIT0DKpO4qhgTlElsm8WgHI9prAVEQ==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.0.0",
|
||||
"tslib": "^1.9.3"
|
||||
}
|
||||
},
|
||||
"@sentry/webpack-plugin": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-1.14.0.tgz",
|
||||
"integrity": "sha512-1cS99mnHqASYtMlHi2J107p6x3lfC5NmLOgA0iI6avaaFes8RTQMlW8YT2CyrvhtQod1bViPZOlh3NOVC8vnOA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sentry/cli": "^1.58.0"
|
||||
}
|
||||
},
|
||||
"@sindresorhus/fnv1a": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-1.2.0.tgz",
|
||||
|
@ -36245,9 +36359,9 @@
|
|||
"integrity": "sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg=="
|
||||
},
|
||||
"codex-tooltip": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.1.tgz",
|
||||
"integrity": "sha512-1xLb1NZbxguNtf02xBRhDphq/EXvMMeEbY0ievjQTHqf8UjXsD41evGk9rqcbjpl+JOjNgtwnp1OaU/X/h6fhQ=="
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codex-tooltip/-/codex-tooltip-1.0.2.tgz",
|
||||
"integrity": "sha512-oC+Bu5X/zyhbPydgMSLWKoM/+vkJMqaLWu3Dt/jZgXS3MWK23INwC5DMBrVXZSufAFk0i0SUni38k9rLMyZn/w=="
|
||||
},
|
||||
"collapse-white-space": {
|
||||
"version": "1.0.6",
|
||||
|
@ -46708,6 +46822,12 @@
|
|||
"ipaddr.js": "1.9.0"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"prr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"npm": ">=6.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@editorjs/editorjs": "^2.19.0",
|
||||
"@editorjs/editorjs": "^2.19.1",
|
||||
"@editorjs/header": "^2.6.1",
|
||||
"@editorjs/image": "^2.6.0",
|
||||
"@editorjs/list": "^1.6.1",
|
||||
|
@ -27,6 +27,7 @@
|
|||
"@material-ui/styles": "^4.5.2",
|
||||
"@saleor/macaw-ui": "^0.1.1-9",
|
||||
"@types/faker": "^5.1.6",
|
||||
"@sentry/react": "^6.0.0",
|
||||
"apollo": "^2.21.2",
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-client": "^2.6.8",
|
||||
|
@ -94,6 +95,7 @@
|
|||
"@pollyjs/adapter-node-http": "^5.0.0",
|
||||
"@pollyjs/core": "^5.0.0",
|
||||
"@pollyjs/persister-fs": "^5.0.0",
|
||||
"@sentry/webpack-plugin": "^1.14.0",
|
||||
"@storybook/addon-storyshots": "^5.2.8",
|
||||
"@storybook/react": "^5.1.9",
|
||||
"@testing-library/react-hooks": "^1.1.0",
|
||||
|
@ -228,6 +230,7 @@
|
|||
"test:e2e:dev": "start-server-and-test start http://localhost:9000 cy:open",
|
||||
"test": "jest src/",
|
||||
"transpile-messages": "node scripts/transpile-tx.js",
|
||||
"lint": "npx eslint \"src/**/*.@(tsx|ts|jsx|js)\" --fix ; npx prettier --check \"src/**/*.@(tsx|ts|jsx|js)\" --write"
|
||||
"lint": "npx eslint \"src/**/*.@(tsx|ts|jsx|js)\" --fix ; npx prettier --check \"src/**/*.@(tsx|ts|jsx|js)\" --write",
|
||||
"postbuild": "rimraf ./build/**/*.js.map"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "f515e15cbc83df73e5bd41437971c2e6",
|
||||
"_id": "a3088678db2635ada66ab049f76c9722",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 691,
|
||||
"bodySize": 702,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
|
@ -28,7 +28,7 @@
|
|||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "691"
|
||||
"value": "702"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
|
@ -56,61 +56,53 @@
|
|||
"postData": {
|
||||
"mimeType": "application/json",
|
||||
"params": [],
|
||||
"text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5NjAyODUxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.eo8_Ew98HICB4cFQN2U7mCJ8ydGVOvQLGRT4CnkufMc\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
"text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMDYzMjg0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU5BPT0iLCJpc19zdGFmZiI6dHJ1ZX0.QDp4vlm1tKhk8iFnY2MnREvO-IubI5j8g_Wylb1XJqc\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
},
|
||||
"queryString": [],
|
||||
"url": "http://localhost:8000/graphql/"
|
||||
},
|
||||
"response": {
|
||||
"bodySize": 1619,
|
||||
"bodySize": 1765,
|
||||
"content": {
|
||||
"mimeType": "application/json",
|
||||
"size": 1619,
|
||||
"text": "[{\"data\": {\"tokenVerify\": {\"payload\": {\"iat\": 1596028218, \"exp\": 1596028518, \"token\": \"C3Sk2kLRVuPA\", \"email\": \"admin@example.com\", \"type\": \"access\", \"user_id\": \"VXNlcjoyMQ==\", \"is_staff\": true}, \"user\": {\"id\": \"VXNlcjoyMQ==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"VerifyToken\"}}}]"
|
||||
"size": 1765,
|
||||
"text": "[{\"data\": {\"tokenVerify\": {\"payload\": {\"iat\": 1610632544, \"exp\": 1610632844, \"token\": \"ksEVMvgg6Bfi\", \"email\": \"admin@example.com\", \"type\": \"access\", \"user_id\": \"VXNlcjoyNA==\", \"is_staff\": true}, \"user\": {\"id\": \"VXNlcjoyNA==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": true, \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES\", \"name\": \"Manage product types and attributes.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"VerifyToken\"}}}]"
|
||||
},
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
"name": "date",
|
||||
"value": "Wed, 29 Jul 2020 13:10:18 GMT"
|
||||
"value": "Thu, 14 Jan 2021 14:10:40 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.1"
|
||||
"value": "WSGIServer/0.2 CPython/3.8.7"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-origin",
|
||||
"value": "http://localhost:9000"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-methods",
|
||||
"value": "POST, OPTIONS"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-headers",
|
||||
"value": "Origin, Content-Type, Accept, Authorization"
|
||||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "1619"
|
||||
"value": "1765"
|
||||
},
|
||||
{
|
||||
"name": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"name": "referrer-policy",
|
||||
"value": "same-origin"
|
||||
}
|
||||
],
|
||||
"headersSize": 336,
|
||||
"headersSize": 194,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2020-07-29T13:10:18.327Z",
|
||||
"time": 23,
|
||||
"startedDateTime": "2021-01-14T14:10:40.434Z",
|
||||
"time": 155,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
|
@ -118,7 +110,7 @@
|
|||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 23
|
||||
"wait": 155
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "7c460842cac4a92c188d5451dfc533a2",
|
||||
"_id": "a2b8a02f624e52cd2b73a831f65d9a52",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 587,
|
||||
"bodySize": 598,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
|
@ -28,7 +28,7 @@
|
|||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "587"
|
||||
"value": "598"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
|
@ -56,74 +56,65 @@
|
|||
"postData": {
|
||||
"mimeType": "application/json",
|
||||
"params": [],
|
||||
"text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
"text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"admin\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
},
|
||||
"queryString": [],
|
||||
"url": "http://localhost:8000/graphql/"
|
||||
},
|
||||
"response": {
|
||||
"bodySize": 1830,
|
||||
"bodySize": 1976,
|
||||
"content": {
|
||||
"mimeType": "application/json",
|
||||
"size": 1830,
|
||||
"text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"rLPNMGNYKXH8VY4UNEWl4nEOFMseocljioigPl36IM2CqbdmOTEpNwvdHBAJ1ZWQ\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5NjAyODUxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU1RPT0iLCJpc19zdGFmZiI6dHJ1ZX0.eo8_Ew98HICB4cFQN2U7mCJ8ydGVOvQLGRT4CnkufMc\", \"user\": {\"id\": \"VXNlcjoyMQ==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]"
|
||||
"size": 1976,
|
||||
"text": "[{\"data\": {\"tokenCreate\": {\"errors\": [], \"csrfToken\": \"UIzzJSFalS8pplfM1j5QNIUNiXb0VFH3kbe6kfTddYvLjJ9DhMasCtHJKXoGDfbw\", \"token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMDYzMjg0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6ImFjY2VzcyIsInVzZXJfaWQiOiJWWE5sY2pveU5BPT0iLCJpc19zdGFmZiI6dHJ1ZX0.QDp4vlm1tKhk8iFnY2MnREvO-IubI5j8g_Wylb1XJqc\", \"user\": {\"id\": \"VXNlcjoyNA==\", \"email\": \"admin@example.com\", \"firstName\": \"\", \"lastName\": \"\", \"isStaff\": true, \"userPermissions\": [{\"code\": \"MANAGE_APPS\", \"name\": \"Manage apps\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_CHECKOUTS\", \"name\": \"Manage checkouts\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_DISCOUNTS\", \"name\": \"Manage sales and vouchers.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_GIFT_CARD\", \"name\": \"Manage gift cards.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_MENUS\", \"name\": \"Manage navigation.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_ORDERS\", \"name\": \"Manage orders.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PAGES\", \"name\": \"Manage pages.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PLUGINS\", \"name\": \"Manage plugins\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES\", \"name\": \"Manage product types and attributes.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_PRODUCTS\", \"name\": \"Manage products.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SETTINGS\", \"name\": \"Manage settings.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_SHIPPING\", \"name\": \"Manage shipping.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_STAFF\", \"name\": \"Manage staff.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_TRANSLATIONS\", \"name\": \"Manage translations.\", \"__typename\": \"UserPermission\"}, {\"code\": \"MANAGE_USERS\", \"name\": \"Manage customers.\", \"__typename\": \"UserPermission\"}], \"avatar\": null, \"__typename\": \"User\"}, \"__typename\": \"CreateToken\"}}}]"
|
||||
},
|
||||
"cookies": [
|
||||
{
|
||||
"httpOnly": true,
|
||||
"name": "refreshToken",
|
||||
"path": "/",
|
||||
"secure": true,
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5ODYyMDIxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6InJMUE5NR05ZS1hIOFZZNFVORVdsNG5FT0ZNc2VvY2xqaW9pZ1BsMzZJTTJDcWJkbU9URXBOd3ZkSEJBSjFaV1EifQ.boD8G4pkSnZF-PLl5oOg85Uj-mqTiAzOkua9aAG3Bz4"
|
||||
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMzIyNDU0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lOQT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6IlVJenpKU0ZhbFM4cHBsZk0xajVRTklVTmlYYjBWRkgza2JlNmtmVGRkWXZMako5RGhNYXNDdEhKS1hvR0RmYncifQ.Br0GWGPPcnysyUxukjBBfXNbwCAm2qlR5OYClwFF3ZQ"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{
|
||||
"name": "date",
|
||||
"value": "Wed, 29 Jul 2020 13:10:18 GMT"
|
||||
"value": "Thu, 14 Jan 2021 13:55:44 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.1"
|
||||
"value": "WSGIServer/0.2 CPython/3.8.7"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-origin",
|
||||
"value": "http://localhost:9000"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-methods",
|
||||
"value": "POST, OPTIONS"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-headers",
|
||||
"value": "Origin, Content-Type, Accept, Authorization"
|
||||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "1830"
|
||||
"value": "1976"
|
||||
},
|
||||
{
|
||||
"name": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"name": "referrer-policy",
|
||||
"value": "same-origin"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
"name": "set-cookie",
|
||||
"value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1OTYwMjgyMTgsImV4cCI6MTU5ODYyMDIxOCwidG9rZW4iOiJDM1NrMmtMUlZ1UEEiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lNUT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6InJMUE5NR05ZS1hIOFZZNFVORVdsNG5FT0ZNc2VvY2xqaW9pZ1BsMzZJTTJDcWJkbU9URXBOd3ZkSEJBSjFaV1EifQ.boD8G4pkSnZF-PLl5oOg85Uj-mqTiAzOkua9aAG3Bz4; HttpOnly; Path=/; Secure"
|
||||
"value": "refreshToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MTA2MzI1NDQsImV4cCI6MTYxMzIyNDU0NCwidG9rZW4iOiJrc0VWTXZnZzZCZmkiLCJlbWFpbCI6ImFkbWluQGV4YW1wbGUuY29tIiwidHlwZSI6InJlZnJlc2giLCJ1c2VyX2lkIjoiVlhObGNqb3lOQT09IiwiaXNfc3RhZmYiOnRydWUsImNzcmZUb2tlbiI6IlVJenpKU0ZhbFM4cHBsZk0xajVRTklVTmlYYjBWRkgza2JlNmtmVGRkWXZMako5RGhNYXNDdEhKS1hvR0RmYncifQ.Br0GWGPPcnysyUxukjBBfXNbwCAm2qlR5OYClwFF3ZQ; HttpOnly; Path=/"
|
||||
}
|
||||
],
|
||||
"headersSize": 768,
|
||||
"headersSize": 618,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2020-07-29T13:10:18.064Z",
|
||||
"time": 118,
|
||||
"startedDateTime": "2021-01-14T13:55:44.094Z",
|
||||
"time": 392,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
|
@ -131,7 +122,7 @@
|
|||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 118
|
||||
"wait": 392
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "4836098613648775386c1e10728424dd",
|
||||
"_id": "b1557b45bbbf7aed1a4a53f5141ca324",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 428,
|
||||
"bodySize": 439,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
|
@ -28,7 +28,7 @@
|
|||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "428"
|
||||
"value": "439"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
|
@ -56,7 +56,7 @@
|
|||
"postData": {
|
||||
"mimeType": "application/json",
|
||||
"params": [],
|
||||
"text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"NotAToken\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
"text": "[{\"operationName\":\"VerifyToken\",\"variables\":{\"token\":\"NotAToken\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation VerifyToken($token: String!) {\\n tokenVerify(token: $token) {\\n payload\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
},
|
||||
"queryString": [],
|
||||
"url": "http://localhost:8000/graphql/"
|
||||
|
@ -72,28 +72,16 @@
|
|||
"headers": [
|
||||
{
|
||||
"name": "date",
|
||||
"value": "Wed, 29 Jul 2020 13:10:18 GMT"
|
||||
"value": "Thu, 14 Jan 2021 14:10:40 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.1"
|
||||
"value": "WSGIServer/0.2 CPython/3.8.7"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-origin",
|
||||
"value": "http://localhost:9000"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-methods",
|
||||
"value": "POST, OPTIONS"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-headers",
|
||||
"value": "Origin, Content-Type, Accept, Authorization"
|
||||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "89"
|
||||
|
@ -101,16 +89,20 @@
|
|||
{
|
||||
"name": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"name": "referrer-policy",
|
||||
"value": "same-origin"
|
||||
}
|
||||
],
|
||||
"headersSize": 334,
|
||||
"headersSize": 192,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2020-07-29T13:10:18.368Z",
|
||||
"time": 6,
|
||||
"startedDateTime": "2021-01-14T14:10:40.611Z",
|
||||
"time": 25,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
|
@ -118,7 +110,7 @@
|
|||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 6
|
||||
"wait": 25
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -8,11 +8,11 @@
|
|||
},
|
||||
"entries": [
|
||||
{
|
||||
"_id": "86487093ff8b070d496fcdc566e01adf",
|
||||
"_id": "d94d7821dc951e48c410d691d7eccdef",
|
||||
"_order": 0,
|
||||
"cache": {},
|
||||
"request": {
|
||||
"bodySize": 603,
|
||||
"bodySize": 614,
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{
|
||||
|
@ -28,7 +28,7 @@
|
|||
{
|
||||
"_fromType": "array",
|
||||
"name": "content-length",
|
||||
"value": "603"
|
||||
"value": "614"
|
||||
},
|
||||
{
|
||||
"_fromType": "array",
|
||||
|
@ -56,7 +56,7 @@
|
|||
"postData": {
|
||||
"mimeType": "application/json",
|
||||
"params": [],
|
||||
"text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"NotAValidPassword123!\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
"text": "[{\"operationName\":\"TokenAuth\",\"variables\":{\"email\":\"admin@example.com\",\"password\":\"NotAValidPassword123!\"},\"query\":\"fragment User on User {\\n id\\n email\\n firstName\\n lastName\\n isStaff\\n userPermissions {\\n code\\n name\\n __typename\\n }\\n avatar {\\n url\\n __typename\\n }\\n __typename\\n}\\n\\nmutation TokenAuth($email: String!, $password: String!) {\\n tokenCreate(email: $email, password: $password) {\\n errors: accountErrors {\\n field\\n message\\n __typename\\n }\\n csrfToken\\n token\\n user {\\n ...User\\n __typename\\n }\\n __typename\\n }\\n}\\n\"}]"
|
||||
},
|
||||
"queryString": [],
|
||||
"url": "http://localhost:8000/graphql/"
|
||||
|
@ -72,28 +72,16 @@
|
|||
"headers": [
|
||||
{
|
||||
"name": "date",
|
||||
"value": "Wed, 29 Jul 2020 13:10:18 GMT"
|
||||
"value": "Thu, 14 Jan 2021 13:55:45 GMT"
|
||||
},
|
||||
{
|
||||
"name": "server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.1"
|
||||
"value": "WSGIServer/0.2 CPython/3.8.7"
|
||||
},
|
||||
{
|
||||
"name": "content-type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-origin",
|
||||
"value": "http://localhost:9000"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-methods",
|
||||
"value": "POST, OPTIONS"
|
||||
},
|
||||
{
|
||||
"name": "access-control-allow-headers",
|
||||
"value": "Origin, Content-Type, Accept, Authorization"
|
||||
},
|
||||
{
|
||||
"name": "content-length",
|
||||
"value": "214"
|
||||
|
@ -101,16 +89,20 @@
|
|||
{
|
||||
"name": "x-content-type-options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"name": "referrer-policy",
|
||||
"value": "same-origin"
|
||||
}
|
||||
],
|
||||
"headersSize": 335,
|
||||
"headersSize": 193,
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"redirectURL": "",
|
||||
"status": 200,
|
||||
"statusText": "OK"
|
||||
},
|
||||
"startedDateTime": "2020-07-29T13:10:18.208Z",
|
||||
"time": 99,
|
||||
"startedDateTime": "2021-01-14T13:55:44.521Z",
|
||||
"time": 1183,
|
||||
"timings": {
|
||||
"blocked": -1,
|
||||
"connect": -1,
|
||||
|
@ -118,7 +110,7 @@
|
|||
"receive": 0,
|
||||
"send": 0,
|
||||
"ssl": -1,
|
||||
"wait": 99
|
||||
"wait": 1183
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
250
schema.graphql
250
schema.graphql
|
@ -393,6 +393,7 @@ type Attribute implements Node & ObjectWithMetadata {
|
|||
privateMetadata: [MetadataItem]!
|
||||
metadata: [MetadataItem]!
|
||||
inputType: AttributeInputTypeEnum
|
||||
entityType: AttributeEntityTypeEnum
|
||||
name: String
|
||||
slug: String
|
||||
type: AttributeTypeEnum
|
||||
|
@ -431,6 +432,7 @@ type AttributeCreate {
|
|||
|
||||
input AttributeCreateInput {
|
||||
inputType: AttributeInputTypeEnum
|
||||
entityType: AttributeEntityTypeEnum
|
||||
name: String!
|
||||
slug: String
|
||||
type: AttributeTypeEnum!
|
||||
|
@ -450,6 +452,11 @@ type AttributeDelete {
|
|||
attribute: Attribute
|
||||
}
|
||||
|
||||
enum AttributeEntityTypeEnum {
|
||||
PAGE
|
||||
PRODUCT
|
||||
}
|
||||
|
||||
type AttributeError {
|
||||
field: String
|
||||
message: String
|
||||
|
@ -490,6 +497,7 @@ enum AttributeInputTypeEnum {
|
|||
DROPDOWN
|
||||
MULTISELECT
|
||||
FILE
|
||||
REFERENCE
|
||||
}
|
||||
|
||||
type AttributeReorderValues {
|
||||
|
@ -563,9 +571,9 @@ type AttributeValue implements Node {
|
|||
id: ID!
|
||||
name: String
|
||||
slug: String
|
||||
type: AttributeValueType @deprecated(reason: "Use the `inputType` field to determine the type of attribute's value. This field will be removed after 2020-07-31.")
|
||||
translation(languageCode: LanguageCodeEnum!): AttributeValueTranslation
|
||||
inputType: AttributeInputTypeEnum
|
||||
reference: ID
|
||||
file: File
|
||||
}
|
||||
|
||||
|
@ -598,6 +606,7 @@ input AttributeValueInput {
|
|||
values: [String]
|
||||
file: String
|
||||
contentType: String
|
||||
references: [ID!]
|
||||
}
|
||||
|
||||
type AttributeValueTranslatableContent implements Node {
|
||||
|
@ -619,13 +628,6 @@ type AttributeValueTranslation implements Node {
|
|||
language: LanguageDisplay!
|
||||
}
|
||||
|
||||
enum AttributeValueType {
|
||||
COLOR
|
||||
GRADIENT
|
||||
URL
|
||||
STRING
|
||||
}
|
||||
|
||||
type AttributeValueUpdate {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
attribute: Attribute
|
||||
|
@ -643,6 +645,7 @@ type BulkProductError {
|
|||
message: String
|
||||
code: ProductErrorCode!
|
||||
attributes: [ID!]
|
||||
values: [ID!]
|
||||
index: Int
|
||||
warehouses: [ID!]
|
||||
channels: [ID!]
|
||||
|
@ -653,6 +656,7 @@ type BulkStockError {
|
|||
message: String
|
||||
code: ProductErrorCode!
|
||||
attributes: [ID!]
|
||||
values: [ID!]
|
||||
index: Int
|
||||
}
|
||||
|
||||
|
@ -667,13 +671,13 @@ type Category implements Node & ObjectWithMetadata {
|
|||
seoDescription: String
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String!
|
||||
descriptionJson: JSONString!
|
||||
description: JSONString!
|
||||
slug: String!
|
||||
parent: Category
|
||||
level: Int!
|
||||
privateMetadata: [MetadataItem]!
|
||||
metadata: [MetadataItem]!
|
||||
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
|
||||
ancestors(before: String, after: String, first: Int, last: Int): CategoryCountableConnection
|
||||
products(channel: String, before: String, after: String, first: Int, last: Int): ProductCountableConnection
|
||||
url: String @deprecated(reason: "This field will be removed after 2020-07-31.")
|
||||
|
@ -717,8 +721,7 @@ input CategoryFilterInput {
|
|||
}
|
||||
|
||||
input CategoryInput {
|
||||
description: String
|
||||
descriptionJson: JSONString
|
||||
description: JSONString
|
||||
name: String
|
||||
slug: String
|
||||
seo: SeoInput
|
||||
|
@ -743,8 +746,8 @@ type CategoryTranslatableContent implements Node {
|
|||
seoDescription: String
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String!
|
||||
descriptionJson: JSONString!
|
||||
description: JSONString!
|
||||
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
|
||||
translation(languageCode: LanguageCodeEnum!): CategoryTranslation
|
||||
category: Category
|
||||
}
|
||||
|
@ -760,9 +763,9 @@ type CategoryTranslation implements Node {
|
|||
seoDescription: String
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String!
|
||||
descriptionJson: JSONString!
|
||||
description: JSONString!
|
||||
language: LanguageDisplay!
|
||||
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
|
||||
}
|
||||
|
||||
type CategoryUpdate {
|
||||
|
@ -1047,11 +1050,11 @@ type Collection implements Node & ObjectWithMetadata {
|
|||
seoDescription: String
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String!
|
||||
descriptionJson: JSONString!
|
||||
description: JSONString!
|
||||
slug: String!
|
||||
privateMetadata: [MetadataItem]!
|
||||
metadata: [MetadataItem]!
|
||||
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
|
||||
products(filter: ProductFilterInput, sortBy: ProductOrder, before: String, after: String, first: Int, last: Int): ProductCountableConnection
|
||||
backgroundImage(size: Int): Image
|
||||
translation(languageCode: LanguageCodeEnum!): CollectionTranslation
|
||||
|
@ -1082,6 +1085,7 @@ type CollectionChannelListingError {
|
|||
message: String
|
||||
code: ProductErrorCode!
|
||||
attributes: [ID!]
|
||||
values: [ID!]
|
||||
channels: [ID!]
|
||||
}
|
||||
|
||||
|
@ -1117,8 +1121,7 @@ input CollectionCreateInput {
|
|||
isPublished: Boolean
|
||||
name: String
|
||||
slug: String
|
||||
description: String
|
||||
descriptionJson: JSONString
|
||||
description: JSONString
|
||||
backgroundImage: Upload
|
||||
backgroundImageAlt: String
|
||||
seo: SeoInput
|
||||
|
@ -1160,8 +1163,7 @@ input CollectionInput {
|
|||
isPublished: Boolean
|
||||
name: String
|
||||
slug: String
|
||||
description: String
|
||||
descriptionJson: JSONString
|
||||
description: JSONString
|
||||
backgroundImage: Upload
|
||||
backgroundImageAlt: String
|
||||
seo: SeoInput
|
||||
|
@ -1203,8 +1205,8 @@ type CollectionTranslatableContent implements Node {
|
|||
seoDescription: String
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String!
|
||||
descriptionJson: JSONString!
|
||||
description: JSONString!
|
||||
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
|
||||
translation(languageCode: LanguageCodeEnum!): CollectionTranslation
|
||||
collection: Collection
|
||||
}
|
||||
|
@ -1220,9 +1222,9 @@ type CollectionTranslation implements Node {
|
|||
seoDescription: String
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String!
|
||||
descriptionJson: JSONString!
|
||||
description: JSONString!
|
||||
language: LanguageDisplay!
|
||||
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
|
||||
}
|
||||
|
||||
type CollectionUpdate {
|
||||
|
@ -1941,6 +1943,49 @@ enum ExportScope {
|
|||
FILTER
|
||||
}
|
||||
|
||||
type ExternalAuthentication {
|
||||
id: String!
|
||||
name: String
|
||||
}
|
||||
|
||||
type ExternalAuthenticationUrl {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
authenticationData: JSONString
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type ExternalLogout {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
logoutData: JSONString
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type ExternalObtainAccessTokens {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
token: String
|
||||
refreshToken: String
|
||||
csrfToken: String
|
||||
user: User
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type ExternalRefresh {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
token: String
|
||||
refreshToken: String
|
||||
csrfToken: String
|
||||
user: User
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type ExternalVerify {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
user: User
|
||||
isValid: Boolean!
|
||||
verifyData: JSONString
|
||||
accountErrors: [AccountError!]!
|
||||
}
|
||||
|
||||
type File {
|
||||
url: String!
|
||||
contentType: String
|
||||
|
@ -1994,9 +2039,21 @@ type FulfillmentRefundProducts {
|
|||
orderErrors: [OrderError!]!
|
||||
}
|
||||
|
||||
type FulfillmentReturnProducts {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
returnFulfillment: Fulfillment
|
||||
replaceFulfillment: Fulfillment
|
||||
order: Order
|
||||
replaceOrder: Order
|
||||
orderErrors: [OrderError!]!
|
||||
}
|
||||
|
||||
enum FulfillmentStatus {
|
||||
FULFILLED
|
||||
REFUNDED
|
||||
RETURNED
|
||||
REPLACED
|
||||
REFUNDED_AND_RETURNED
|
||||
CANCELED
|
||||
}
|
||||
|
||||
|
@ -2295,10 +2352,12 @@ type Margin {
|
|||
stop: Int
|
||||
}
|
||||
|
||||
type Menu implements Node {
|
||||
type Menu implements Node & ObjectWithMetadata {
|
||||
id: ID!
|
||||
name: String!
|
||||
slug: String!
|
||||
privateMetadata: [MetadataItem]!
|
||||
metadata: [MetadataItem]!
|
||||
items: [MenuItem]
|
||||
}
|
||||
|
||||
|
@ -2365,7 +2424,7 @@ input MenuInput {
|
|||
slug: String
|
||||
}
|
||||
|
||||
type MenuItem implements Node {
|
||||
type MenuItem implements Node & ObjectWithMetadata {
|
||||
id: ID!
|
||||
name: String!
|
||||
menu: Menu!
|
||||
|
@ -2374,6 +2433,8 @@ type MenuItem implements Node {
|
|||
collection: Collection
|
||||
page: Page
|
||||
level: Int!
|
||||
privateMetadata: [MetadataItem]!
|
||||
metadata: [MetadataItem]!
|
||||
children: [MenuItem]
|
||||
url: String
|
||||
translation(languageCode: LanguageCodeEnum!): MenuItemTranslation
|
||||
|
@ -2596,6 +2657,7 @@ type Mutation {
|
|||
productTypeBulkDelete(ids: [ID]!): ProductTypeBulkDelete
|
||||
productTypeUpdate(id: ID!, input: ProductTypeInput!): ProductTypeUpdate
|
||||
productTypeReorderAttributes(moves: [ReorderInput]!, productTypeId: ID!, type: ProductAttributeType!): ProductTypeReorderAttributes
|
||||
productReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, productId: ID!): ProductReorderAttributeValues
|
||||
digitalContentCreate(input: DigitalContentUploadInput!, variantId: ID!): DigitalContentCreate
|
||||
digitalContentDelete(variantId: ID!): DigitalContentDelete
|
||||
digitalContentUpdate(input: DigitalContentInput!, variantId: ID!): DigitalContentUpdate
|
||||
|
@ -2611,6 +2673,7 @@ type Mutation {
|
|||
productVariantSetDefault(productId: ID!, variantId: ID!): ProductVariantSetDefault
|
||||
productVariantTranslate(id: ID!, input: NameTranslationInput!, languageCode: LanguageCodeEnum!): ProductVariantTranslate
|
||||
productVariantChannelListingUpdate(id: ID!, input: [ProductVariantChannelListingAddInput!]!): ProductVariantChannelListingUpdate
|
||||
productVariantReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, variantId: ID!): ProductVariantReorderAttributeValues
|
||||
variantImageAssign(imageId: ID!, variantId: ID!): VariantImageAssign
|
||||
variantImageUnassign(imageId: ID!, variantId: ID!): VariantImageUnassign
|
||||
paymentCapture(amount: PositiveDecimal, paymentId: ID!): PaymentCapture
|
||||
|
@ -2630,6 +2693,7 @@ type Mutation {
|
|||
pageAttributeAssign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeAssign
|
||||
pageAttributeUnassign(attributeIds: [ID!]!, pageTypeId: ID!): PageAttributeUnassign
|
||||
pageTypeReorderAttributes(moves: [ReorderInput!]!, pageTypeId: ID!): PageTypeReorderAttributes
|
||||
pageReorderAttributeValues(attributeId: ID!, moves: [ReorderInput]!, pageId: ID!): PageReorderAttributeValues
|
||||
draftOrderComplete(id: ID!): DraftOrderComplete
|
||||
draftOrderCreate(input: DraftOrderCreateInput!): DraftOrderCreate
|
||||
draftOrderDelete(id: ID!): DraftOrderDelete
|
||||
|
@ -2647,6 +2711,7 @@ type Mutation {
|
|||
orderFulfillmentCancel(id: ID!, input: FulfillmentCancelInput!): FulfillmentCancel
|
||||
orderFulfillmentUpdateTracking(id: ID!, input: FulfillmentUpdateTrackingInput!): FulfillmentUpdateTracking
|
||||
orderFulfillmentRefundProducts(input: OrderRefundProductsInput!, order: ID!): FulfillmentRefundProducts
|
||||
orderFulfillmentReturnProducts(input: OrderReturnProductsInput!, order: ID!): FulfillmentReturnProducts
|
||||
orderMarkAsPaid(id: ID!, transactionReference: String): OrderMarkAsPaid
|
||||
orderRefund(amount: PositiveDecimal!, id: ID!): OrderRefund
|
||||
orderUpdate(id: ID!, input: OrderUpdateInput!): OrderUpdate
|
||||
|
@ -2743,6 +2808,11 @@ type Mutation {
|
|||
tokenRefresh(csrfToken: String, refreshToken: String): RefreshToken
|
||||
tokenVerify(token: String!): VerifyToken
|
||||
tokensDeactivateAll: DeactivateAllUserTokens
|
||||
externalAuthenticationUrl(input: JSONString!, pluginId: String!): ExternalAuthenticationUrl
|
||||
externalObtainAccessTokens(input: JSONString!, pluginId: String!): ExternalObtainAccessTokens
|
||||
externalRefresh(input: JSONString!, pluginId: String!): ExternalRefresh
|
||||
externalLogout(input: JSONString!, pluginId: String!): ExternalLogout
|
||||
externalVerify(input: JSONString!, pluginId: String!): ExternalVerify
|
||||
requestPasswordReset(email: String!, redirectUrl: String!): RequestPasswordReset
|
||||
confirmAccount(email: String!, token: String!): ConfirmAccount
|
||||
setPassword(email: String!, password: String!, token: String!): SetPassword
|
||||
|
@ -2812,7 +2882,8 @@ type Order implements Node & ObjectWithMetadata {
|
|||
shippingMethod: ShippingMethod
|
||||
shippingMethodName: String
|
||||
channel: Channel!
|
||||
shippingPrice: TaxedMoney
|
||||
shippingPrice: TaxedMoney!
|
||||
shippingTaxRate: Float!
|
||||
token: String!
|
||||
voucher: Voucher
|
||||
giftCards: [GiftCard]
|
||||
|
@ -2831,16 +2902,16 @@ type Order implements Node & ObjectWithMetadata {
|
|||
availableShippingMethods: [ShippingMethod]
|
||||
invoices: [Invoice]
|
||||
number: String
|
||||
isPaid: Boolean
|
||||
paymentStatus: PaymentChargeStatusEnum
|
||||
paymentStatusDisplay: String
|
||||
isPaid: Boolean!
|
||||
paymentStatus: PaymentChargeStatusEnum!
|
||||
paymentStatusDisplay: String!
|
||||
payments: [Payment]
|
||||
total: TaxedMoney
|
||||
subtotal: TaxedMoney
|
||||
total: TaxedMoney!
|
||||
subtotal: TaxedMoney!
|
||||
statusDisplay: String
|
||||
canFinalize: Boolean!
|
||||
totalAuthorized: Money
|
||||
totalCaptured: Money
|
||||
totalAuthorized: Money!
|
||||
totalCaptured: Money!
|
||||
events: [OrderEvent]
|
||||
totalBalance: Money!
|
||||
userEmail: String
|
||||
|
@ -2909,6 +2980,7 @@ input OrderDraftFilterInput {
|
|||
customer: String
|
||||
created: DateRangeInput
|
||||
search: String
|
||||
channels: [ID]
|
||||
}
|
||||
|
||||
type OrderError {
|
||||
|
@ -2944,8 +3016,7 @@ enum OrderErrorCode {
|
|||
UNIQUE
|
||||
VOID_INACTIVE_PAYMENT
|
||||
ZERO_QUANTITY
|
||||
INVALID_REFUND_QUANTITY
|
||||
CANNOT_REFUND_FULFILLMENT_LINE
|
||||
INVALID_QUANTITY
|
||||
INSUFFICIENT_STOCK
|
||||
DUPLICATED_INPUT_ITEM
|
||||
NOT_AVAILABLE_IN_CHANNEL
|
||||
|
@ -2973,6 +3044,7 @@ type OrderEvent implements Node {
|
|||
warehouse: Warehouse
|
||||
transactionReference: String
|
||||
shippingCostsIncluded: Boolean
|
||||
relatedOrder: Order
|
||||
}
|
||||
|
||||
type OrderEventCountableConnection {
|
||||
|
@ -3006,6 +3078,7 @@ enum OrderEventsEmailsEnum {
|
|||
|
||||
enum OrderEventsEnum {
|
||||
DRAFT_CREATED
|
||||
DRAFT_CREATED_FROM_REPLACE
|
||||
DRAFT_ADDED_PRODUCTS
|
||||
DRAFT_REMOVED_PRODUCTS
|
||||
PLACED
|
||||
|
@ -3014,6 +3087,7 @@ enum OrderEventsEnum {
|
|||
CANCELED
|
||||
ORDER_MARKED_AS_PAID
|
||||
ORDER_FULLY_PAID
|
||||
ORDER_REPLACEMENT_CREATED
|
||||
UPDATED_ADDRESS
|
||||
EMAIL_SENT
|
||||
CONFIRMED
|
||||
|
@ -3031,6 +3105,8 @@ enum OrderEventsEnum {
|
|||
FULFILLMENT_RESTOCKED_ITEMS
|
||||
FULFILLMENT_FULFILLED_ITEMS
|
||||
FULFILLMENT_REFUNDED
|
||||
FULFILLMENT_RETURNED
|
||||
FULFILLMENT_REPLACED
|
||||
TRACKING_UPDATED
|
||||
NOTE_ADDED
|
||||
OTHER
|
||||
|
@ -3078,8 +3154,8 @@ type OrderLine implements Node {
|
|||
taxRate: Float!
|
||||
digitalContentUrl: DigitalContentUrl
|
||||
thumbnail(size: Int): Image
|
||||
unitPrice: TaxedMoney
|
||||
totalPrice: TaxedMoney
|
||||
unitPrice: TaxedMoney!
|
||||
totalPrice: TaxedMoney!
|
||||
variant: ProductVariant
|
||||
translatedProductName: String!
|
||||
translatedVariantName: String!
|
||||
|
@ -3124,6 +3200,26 @@ input OrderRefundProductsInput {
|
|||
includeShippingCosts: Boolean = false
|
||||
}
|
||||
|
||||
input OrderReturnFulfillmentLineInput {
|
||||
fulfillmentLineId: ID!
|
||||
quantity: Int!
|
||||
replace: Boolean = false
|
||||
}
|
||||
|
||||
input OrderReturnLineInput {
|
||||
orderLineId: ID!
|
||||
quantity: Int!
|
||||
replace: Boolean = false
|
||||
}
|
||||
|
||||
input OrderReturnProductsInput {
|
||||
orderLines: [OrderReturnLineInput!]
|
||||
fulfillmentLines: [OrderReturnFulfillmentLineInput!]
|
||||
amountToRefund: PositiveDecimal
|
||||
includeShippingCosts: Boolean = false
|
||||
refund: Boolean = false
|
||||
}
|
||||
|
||||
type OrderSettings {
|
||||
automaticallyConfirmAllNewOrders: Boolean!
|
||||
}
|
||||
|
@ -3166,6 +3262,8 @@ enum OrderStatus {
|
|||
UNCONFIRMED
|
||||
UNFULFILLED
|
||||
PARTIALLY_FULFILLED
|
||||
PARTIALLY_RETURNED
|
||||
RETURNED
|
||||
FULFILLED
|
||||
CANCELED
|
||||
}
|
||||
|
@ -3213,8 +3311,7 @@ type Page implements Node & ObjectWithMetadata {
|
|||
seoDescription: String
|
||||
id: ID!
|
||||
title: String!
|
||||
content: String!
|
||||
contentJson: JSONString!
|
||||
content: JSONString!
|
||||
publicationDate: Date
|
||||
isPublished: Boolean!
|
||||
slug: String!
|
||||
|
@ -3222,6 +3319,7 @@ type Page implements Node & ObjectWithMetadata {
|
|||
created: DateTime!
|
||||
privateMetadata: [MetadataItem]!
|
||||
metadata: [MetadataItem]!
|
||||
contentJson: String! @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.")
|
||||
translation(languageCode: LanguageCodeEnum!): PageTranslation
|
||||
attributes: [SelectedAttribute!]!
|
||||
}
|
||||
|
@ -3270,8 +3368,7 @@ type PageCreate {
|
|||
input PageCreateInput {
|
||||
slug: String
|
||||
title: String
|
||||
content: String
|
||||
contentJson: JSONString
|
||||
content: JSONString
|
||||
attributes: [AttributeValueInput!]
|
||||
isPublished: Boolean
|
||||
publicationDate: String
|
||||
|
@ -3290,6 +3387,7 @@ type PageError {
|
|||
message: String
|
||||
code: PageErrorCode!
|
||||
attributes: [ID!]
|
||||
values: [ID!]
|
||||
}
|
||||
|
||||
enum PageErrorCode {
|
||||
|
@ -3316,14 +3414,19 @@ type PageInfo {
|
|||
input PageInput {
|
||||
slug: String
|
||||
title: String
|
||||
content: String
|
||||
contentJson: JSONString
|
||||
content: JSONString
|
||||
attributes: [AttributeValueInput!]
|
||||
isPublished: Boolean
|
||||
publicationDate: String
|
||||
seo: SeoInput
|
||||
}
|
||||
|
||||
type PageReorderAttributeValues {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
page: Page
|
||||
pageErrors: [PageError!]!
|
||||
}
|
||||
|
||||
enum PageSortField {
|
||||
TITLE
|
||||
SLUG
|
||||
|
@ -3342,8 +3445,8 @@ type PageTranslatableContent implements Node {
|
|||
seoDescription: String
|
||||
id: ID!
|
||||
title: String!
|
||||
content: String!
|
||||
contentJson: JSONString!
|
||||
content: JSONString!
|
||||
contentJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.")
|
||||
translation(languageCode: LanguageCodeEnum!): PageTranslation
|
||||
page: Page
|
||||
}
|
||||
|
@ -3359,17 +3462,16 @@ type PageTranslation implements Node {
|
|||
seoDescription: String
|
||||
id: ID!
|
||||
title: String!
|
||||
content: String!
|
||||
contentJson: JSONString!
|
||||
content: JSONString!
|
||||
language: LanguageDisplay!
|
||||
contentJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `content` field instead.")
|
||||
}
|
||||
|
||||
input PageTranslationInput {
|
||||
seoTitle: String
|
||||
seoDescription: String
|
||||
title: String
|
||||
content: String
|
||||
contentJson: JSONString
|
||||
content: JSONString
|
||||
}
|
||||
|
||||
type PageType implements Node & ObjectWithMetadata {
|
||||
|
@ -3737,8 +3839,7 @@ type Product implements Node & ObjectWithMetadata {
|
|||
seoTitle: String
|
||||
seoDescription: String
|
||||
name: String!
|
||||
description: String!
|
||||
descriptionJson: JSONString!
|
||||
description: JSONString!
|
||||
productType: ProductType!
|
||||
slug: String!
|
||||
category: Category
|
||||
|
@ -3749,6 +3850,7 @@ type Product implements Node & ObjectWithMetadata {
|
|||
rating: Float
|
||||
privateMetadata: [MetadataItem]!
|
||||
metadata: [MetadataItem]!
|
||||
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
|
||||
url: String! @deprecated(reason: "This field will be removed after 2020-07-31.")
|
||||
thumbnail(size: Int): Image
|
||||
pricing: ProductPricingInfo
|
||||
|
@ -3821,6 +3923,7 @@ type ProductChannelListingError {
|
|||
message: String
|
||||
code: ProductErrorCode!
|
||||
attributes: [ID!]
|
||||
values: [ID!]
|
||||
channels: [ID!]
|
||||
}
|
||||
|
||||
|
@ -3857,8 +3960,7 @@ input ProductCreateInput {
|
|||
category: ID
|
||||
chargeTaxes: Boolean
|
||||
collections: [ID]
|
||||
description: String
|
||||
descriptionJson: JSONString
|
||||
description: JSONString
|
||||
name: String
|
||||
slug: String
|
||||
taxCode: String
|
||||
|
@ -3879,6 +3981,7 @@ type ProductError {
|
|||
message: String
|
||||
code: ProductErrorCode!
|
||||
attributes: [ID!]
|
||||
values: [ID!]
|
||||
}
|
||||
|
||||
enum ProductErrorCode {
|
||||
|
@ -3988,8 +4091,7 @@ input ProductInput {
|
|||
category: ID
|
||||
chargeTaxes: Boolean
|
||||
collections: [ID]
|
||||
description: String
|
||||
descriptionJson: JSONString
|
||||
description: JSONString
|
||||
name: String
|
||||
slug: String
|
||||
taxCode: String
|
||||
|
@ -4026,6 +4128,12 @@ type ProductPricingInfo {
|
|||
priceRangeLocalCurrency: TaxedMoneyRange
|
||||
}
|
||||
|
||||
type ProductReorderAttributeValues {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
product: Product
|
||||
productErrors: [ProductError!]!
|
||||
}
|
||||
|
||||
input ProductStockFilterInput {
|
||||
warehouseIds: [ID!]
|
||||
quantity: IntRangeInput
|
||||
|
@ -4036,8 +4144,8 @@ type ProductTranslatableContent implements Node {
|
|||
seoTitle: String
|
||||
seoDescription: String
|
||||
name: String!
|
||||
description: String!
|
||||
descriptionJson: JSONString!
|
||||
description: JSONString!
|
||||
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
|
||||
translation(languageCode: LanguageCodeEnum!): ProductTranslation
|
||||
product: Product
|
||||
}
|
||||
|
@ -4053,9 +4161,9 @@ type ProductTranslation implements Node {
|
|||
seoTitle: String
|
||||
seoDescription: String
|
||||
name: String!
|
||||
description: String!
|
||||
descriptionJson: JSONString!
|
||||
description: JSONString!
|
||||
language: LanguageDisplay!
|
||||
descriptionJson: String @deprecated(reason: "Will be removed in Saleor 4.0. Use the `description` field instead.")
|
||||
}
|
||||
|
||||
type ProductType implements Node & ObjectWithMetadata {
|
||||
|
@ -4278,6 +4386,12 @@ type ProductVariantReorder {
|
|||
productErrors: [ProductError!]!
|
||||
}
|
||||
|
||||
type ProductVariantReorderAttributeValues {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
productVariant: ProductVariant
|
||||
productErrors: [ProductError!]!
|
||||
}
|
||||
|
||||
type ProductVariantSetDefault {
|
||||
errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.")
|
||||
product: Product
|
||||
|
@ -4405,7 +4519,7 @@ type Query {
|
|||
permissionGroup(id: ID!): Group
|
||||
me: User
|
||||
staffUsers(filter: StaffUserInput, sortBy: UserSortingInput, before: String, after: String, first: Int, last: Int): UserCountableConnection
|
||||
user(id: ID!): User
|
||||
user(id: ID, email: String): User
|
||||
_entities(representations: [_Any]): [_Entity]
|
||||
_service: _Service
|
||||
}
|
||||
|
@ -4840,6 +4954,7 @@ input ShippingZoneUpdateInput {
|
|||
|
||||
type Shop {
|
||||
availablePaymentGateways(currency: String): [PaymentGateway!]!
|
||||
availableExternalAuthentications: [ExternalAuthentication!]!
|
||||
availableShippingMethods(channel: String!, address: AddressInput): [ShippingMethod]
|
||||
geolocalization: Geolocalization
|
||||
countries(languageCode: LanguageCodeEnum): [CountryDisplay!]!
|
||||
|
@ -5215,8 +5330,7 @@ input TranslationInput {
|
|||
seoTitle: String
|
||||
seoDescription: String
|
||||
name: String
|
||||
description: String
|
||||
descriptionJson: JSONString
|
||||
description: JSONString
|
||||
}
|
||||
|
||||
scalar UUID
|
||||
|
@ -5547,7 +5661,7 @@ type VoucherUpdate {
|
|||
voucher: Voucher
|
||||
}
|
||||
|
||||
type Warehouse implements Node {
|
||||
type Warehouse implements Node & ObjectWithMetadata {
|
||||
id: ID!
|
||||
name: String!
|
||||
slug: String!
|
||||
|
@ -5555,6 +5669,8 @@ type Warehouse implements Node {
|
|||
shippingZones(before: String, after: String, first: Int, last: Int): ShippingZoneCountableConnection!
|
||||
address: Address!
|
||||
email: String!
|
||||
privateMetadata: [MetadataItem]!
|
||||
metadata: [MetadataItem]!
|
||||
}
|
||||
|
||||
input WarehouseAddressInput {
|
||||
|
@ -5779,4 +5895,4 @@ union _Entity = Address | User | Group | App | ProductVariant | Product | Produc
|
|||
|
||||
type _Service {
|
||||
sdl: String
|
||||
}
|
||||
}
|
|
@ -79,6 +79,7 @@ export const appDeleteFailedInstallationMutation = gql`
|
|||
`;
|
||||
|
||||
export const appFetchMutation = gql`
|
||||
${appErrorFragment}
|
||||
mutation AppFetch($manifestUrl: String!) {
|
||||
appFetchManifest(manifestUrl: $manifestUrl) {
|
||||
manifest {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
|
||||
|
@ -7,16 +8,90 @@ import FormSpacer from "@saleor/components/FormSpacer";
|
|||
import SingleSelectField from "@saleor/components/SingleSelectField";
|
||||
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
|
||||
import {
|
||||
AttributeEntityTypeEnum,
|
||||
AttributeInputTypeEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
import { getFormErrors } from "@saleor/utils/errors";
|
||||
import getAttributeErrorMessage from "@saleor/utils/errors/attribute";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { defineMessages, useIntl } from "react-intl";
|
||||
import slugify from "slugify";
|
||||
|
||||
import { getAttributeSlugErrorMessage } from "../../errors";
|
||||
import { AttributePageFormData } from "../AttributePage";
|
||||
|
||||
const messages = defineMessages({
|
||||
attributeLabel: {
|
||||
defaultMessage: "Default Label",
|
||||
description: "attribute's label"
|
||||
},
|
||||
attributeSlug: {
|
||||
defaultMessage: "Attribute Code",
|
||||
description: "attribute's slug short code label"
|
||||
},
|
||||
attributeSlugHelperText: {
|
||||
defaultMessage: "This is used internally. Make sure you don’t use spaces",
|
||||
description: "attribute slug input field helper text"
|
||||
},
|
||||
entityType: {
|
||||
defaultMessage: "Entity",
|
||||
description: "attribute's editor component entity"
|
||||
},
|
||||
inputType: {
|
||||
defaultMessage: "Catalog Input type for Store Owner",
|
||||
description: "attribute's editor component"
|
||||
},
|
||||
valueRequired: {
|
||||
defaultMessage: "Value Required",
|
||||
description: "check to require attribute to have value"
|
||||
}
|
||||
});
|
||||
|
||||
const inputTypeMessages = defineMessages({
|
||||
dropdown: {
|
||||
defaultMessage: "Dropdown",
|
||||
description: "product attribute type"
|
||||
},
|
||||
file: {
|
||||
defaultMessage: "File",
|
||||
description: "file attribute type"
|
||||
},
|
||||
multiselect: {
|
||||
defaultMessage: "Multiple Select",
|
||||
description: "product attribute type"
|
||||
},
|
||||
references: {
|
||||
defaultMessage: "References",
|
||||
description: "references attribute type"
|
||||
}
|
||||
});
|
||||
|
||||
const entityTypeMessages = defineMessages({
|
||||
page: {
|
||||
defaultMessage: "Pages",
|
||||
description: "page attribute entity type"
|
||||
},
|
||||
product: {
|
||||
defaultMessage: "Products",
|
||||
description: "product attribute entity type"
|
||||
}
|
||||
});
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
inputTypeSection: {
|
||||
columnGap: theme.spacing(2) + "px",
|
||||
display: "flex",
|
||||
[theme.breakpoints.down("md")]: {
|
||||
flexFlow: "wrap",
|
||||
rowGap: theme.spacing(3) + "px"
|
||||
}
|
||||
}
|
||||
}),
|
||||
{ name: "AttributeDetails" }
|
||||
);
|
||||
|
||||
export interface AttributeDetailsProps {
|
||||
canChangeType: boolean;
|
||||
data: AttributePageFormData;
|
||||
|
@ -25,39 +100,43 @@ export interface AttributeDetailsProps {
|
|||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
}
|
||||
|
||||
const AttributeDetails: React.FC<AttributeDetailsProps> = ({
|
||||
canChangeType,
|
||||
data,
|
||||
disabled,
|
||||
errors,
|
||||
onChange
|
||||
}) => {
|
||||
const AttributeDetails: React.FC<AttributeDetailsProps> = props => {
|
||||
const { canChangeType, data, disabled, errors, onChange } = props;
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
const inputTypeChoices = [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Dropdown",
|
||||
description: "product attribute type"
|
||||
}),
|
||||
label: intl.formatMessage(inputTypeMessages.dropdown),
|
||||
value: AttributeInputTypeEnum.DROPDOWN
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "Multiple Select",
|
||||
description: "product attribute type"
|
||||
}),
|
||||
label: intl.formatMessage(inputTypeMessages.multiselect),
|
||||
value: AttributeInputTypeEnum.MULTISELECT
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
defaultMessage: "File",
|
||||
description: "file attribute type"
|
||||
}),
|
||||
label: intl.formatMessage(inputTypeMessages.file),
|
||||
value: AttributeInputTypeEnum.FILE
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(inputTypeMessages.references),
|
||||
value: AttributeInputTypeEnum.REFERENCE
|
||||
}
|
||||
];
|
||||
const entityTypeChoices = [
|
||||
{
|
||||
label: intl.formatMessage(entityTypeMessages.page),
|
||||
value: AttributeEntityTypeEnum.PAGE
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage(entityTypeMessages.product),
|
||||
value: AttributeEntityTypeEnum.PRODUCT
|
||||
}
|
||||
];
|
||||
|
||||
const formErrors = getFormErrors(["name", "slug", "inputType"], errors);
|
||||
const formErrors = getFormErrors(
|
||||
["name", "slug", "inputType", "entityType"],
|
||||
errors
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
@ -68,10 +147,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({
|
|||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!formErrors.name}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Default Label",
|
||||
description: "attribute's label"
|
||||
})}
|
||||
label={intl.formatMessage(messages.attributeLabel)}
|
||||
name={"name" as keyof AttributePageFormData}
|
||||
fullWidth
|
||||
helperText={getAttributeErrorMessage(formErrors.name, intl)}
|
||||
|
@ -82,45 +158,46 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({
|
|||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!formErrors.slug}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Attribute Code",
|
||||
description: "attribute's slug short code label"
|
||||
})}
|
||||
label={intl.formatMessage(messages.attributeSlug)}
|
||||
name={"slug" as keyof AttributePageFormData}
|
||||
placeholder={slugify(data.name).toLowerCase()}
|
||||
fullWidth
|
||||
helperText={
|
||||
getAttributeSlugErrorMessage(formErrors.slug, intl) ||
|
||||
intl.formatMessage({
|
||||
defaultMessage:
|
||||
"This is used internally. Make sure you don’t use spaces",
|
||||
description: "attribute slug input field helper text"
|
||||
})
|
||||
intl.formatMessage(messages.attributeSlugHelperText)
|
||||
}
|
||||
value={data.slug}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<SingleSelectField
|
||||
choices={inputTypeChoices}
|
||||
disabled={disabled || !canChangeType}
|
||||
error={!!formErrors.inputType}
|
||||
hint={getAttributeErrorMessage(formErrors.inputType, intl)}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Catalog Input type for Store Owner",
|
||||
description: "attribute's editor component"
|
||||
})}
|
||||
name="inputType"
|
||||
onChange={onChange}
|
||||
value={data.inputType}
|
||||
/>
|
||||
<div className={classes.inputTypeSection}>
|
||||
<SingleSelectField
|
||||
choices={inputTypeChoices}
|
||||
disabled={disabled || !canChangeType}
|
||||
error={!!formErrors.inputType}
|
||||
hint={getAttributeErrorMessage(formErrors.inputType, intl)}
|
||||
label={intl.formatMessage(messages.inputType)}
|
||||
name="inputType"
|
||||
onChange={onChange}
|
||||
value={data.inputType}
|
||||
/>
|
||||
{data.inputType === AttributeInputTypeEnum.REFERENCE && (
|
||||
<SingleSelectField
|
||||
choices={entityTypeChoices}
|
||||
disabled={disabled || !canChangeType}
|
||||
error={!!formErrors.entityType}
|
||||
hint={getAttributeErrorMessage(formErrors.entityType, intl)}
|
||||
label={intl.formatMessage(messages.entityType)}
|
||||
name="entityType"
|
||||
onChange={onChange}
|
||||
value={data.entityType}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<FormSpacer />
|
||||
<ControlledCheckbox
|
||||
name={"valueRequired" as keyof AttributePageFormData}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Value Required",
|
||||
description: "check to require attribute to have value"
|
||||
})}
|
||||
label={intl.formatMessage(messages.valueRequired)}
|
||||
checked={data.valueRequired}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES } from "@saleor/attributes/utils/data";
|
||||
import AppHeader from "@saleor/components/AppHeader";
|
||||
import CardSpacer from "@saleor/components/CardSpacer";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
|
||||
|
@ -17,6 +18,7 @@ import { sectionNames } from "@saleor/intl";
|
|||
import { maybe } from "@saleor/misc";
|
||||
import { ReorderAction } from "@saleor/types";
|
||||
import {
|
||||
AttributeEntityTypeEnum,
|
||||
AttributeInputTypeEnum,
|
||||
AttributeTypeEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
|
@ -51,6 +53,7 @@ export interface AttributePageFormData extends MetadataFormData {
|
|||
availableInGrid: boolean;
|
||||
filterableInDashboard: boolean;
|
||||
inputType: AttributeInputTypeEnum;
|
||||
entityType: AttributeEntityTypeEnum;
|
||||
filterableInStorefront: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
|
@ -84,6 +87,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
|
|||
attribute === null
|
||||
? {
|
||||
availableInGrid: true,
|
||||
entityType: null,
|
||||
filterableInDashboard: true,
|
||||
filterableInStorefront: true,
|
||||
inputType: AttributeInputTypeEnum.DROPDOWN,
|
||||
|
@ -98,6 +102,7 @@ const AttributePage: React.FC<AttributePageProps> = ({
|
|||
}
|
||||
: {
|
||||
availableInGrid: maybe(() => attribute.availableInGrid, true),
|
||||
entityType: attribute?.entityType ?? null,
|
||||
filterableInDashboard: maybe(
|
||||
() => attribute.filterableInDashboard,
|
||||
true
|
||||
|
@ -172,7 +177,9 @@ const AttributePage: React.FC<AttributePageProps> = ({
|
|||
errors={errors}
|
||||
onChange={change}
|
||||
/>
|
||||
{data.inputType !== AttributeInputTypeEnum.FILE && (
|
||||
{ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes(
|
||||
data.inputType
|
||||
) && (
|
||||
<>
|
||||
<CardSpacer />
|
||||
<AttributeValues
|
||||
|
|
|
@ -2,6 +2,7 @@ import Card from "@material-ui/core/Card";
|
|||
import CardContent from "@material-ui/core/CardContent";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import { ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES } from "@saleor/attributes/utils/data";
|
||||
import CardSpacer from "@saleor/components/CardSpacer";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import ControlledCheckbox from "@saleor/components/ControlledCheckbox";
|
||||
|
@ -10,17 +11,59 @@ import FormSpacer from "@saleor/components/FormSpacer";
|
|||
import Hr from "@saleor/components/Hr";
|
||||
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import {
|
||||
AttributeInputTypeEnum,
|
||||
AttributeTypeEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
import { AttributeTypeEnum } from "@saleor/types/globalTypes";
|
||||
import { getFormErrors } from "@saleor/utils/errors";
|
||||
import getAttributeErrorMessage from "@saleor/utils/errors/attribute";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { AttributePageFormData } from "../AttributePage";
|
||||
|
||||
const messages = defineMessages({
|
||||
availableInGrid: {
|
||||
defaultMessage: "Add to Column Options",
|
||||
description: "add attribute as column in product list table"
|
||||
},
|
||||
availableInGridCaption: {
|
||||
defaultMessage:
|
||||
"If enabled this attribute can be used as a column in product table.",
|
||||
description: "caption"
|
||||
},
|
||||
dashboardPropertiesTitle: {
|
||||
defaultMessage: "Dashboard Properties",
|
||||
description: "attribute properties regarding dashboard"
|
||||
},
|
||||
filterableInDashboard: {
|
||||
defaultMessage: "Use in Filtering",
|
||||
description: "use attribute in filtering"
|
||||
},
|
||||
filterableInDashboardCaption: {
|
||||
defaultMessage:
|
||||
"If enabled, you’ll be able to use this attribute to filter products in product list.",
|
||||
description: "caption"
|
||||
},
|
||||
filterableInStorefront: {
|
||||
defaultMessage: "Use in Faceted Navigation",
|
||||
description: "attribute is filterable in storefront"
|
||||
},
|
||||
storefrontPropertiesTitle: {
|
||||
defaultMessage: "Storefront Properties",
|
||||
description: "attribute properties regarding storefront"
|
||||
},
|
||||
storefrontSearchPosition: {
|
||||
defaultMessage: "Position in faceted navigation",
|
||||
description: "attribute position in storefront filters"
|
||||
},
|
||||
visibleInStorefront: {
|
||||
defaultMessage: "Public",
|
||||
description: "attribute visibility in storefront"
|
||||
},
|
||||
visibleInStorefrontCaption: {
|
||||
defaultMessage: "If enabled, attribute will be accessible to customers.",
|
||||
description: "caption"
|
||||
}
|
||||
});
|
||||
|
||||
export interface AttributePropertiesProps {
|
||||
data: AttributePageFormData;
|
||||
disabled: boolean;
|
||||
|
@ -38,6 +81,14 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
|
|||
|
||||
const formErrors = getFormErrors(["storefrontSearchPosition"], errors);
|
||||
|
||||
const dashboardProperties = ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes(
|
||||
data.inputType
|
||||
);
|
||||
|
||||
const storefrontFacetedNavigationProperties =
|
||||
ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes(data.inputType) &&
|
||||
data.type === AttributeTypeEnum.PRODUCT_TYPE;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle title={intl.formatMessage(commonMessages.properties)} />
|
||||
|
@ -74,61 +125,48 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
|
|||
/> */}
|
||||
|
||||
<Typography variant="subtitle1">
|
||||
<FormattedMessage
|
||||
defaultMessage="Storefront Properties"
|
||||
description="attribute properties regarding storefront"
|
||||
/>
|
||||
<FormattedMessage {...messages.storefrontPropertiesTitle} />
|
||||
</Typography>
|
||||
<Hr />
|
||||
{data.inputType !== AttributeInputTypeEnum.FILE &&
|
||||
data.type === AttributeTypeEnum.PRODUCT_TYPE && (
|
||||
<>
|
||||
<ControlledCheckbox
|
||||
name={"filterableInStorefront" as keyof FormData}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Use in Faceted Navigation",
|
||||
description: "attribute is filterable in storefront"
|
||||
})}
|
||||
checked={data.filterableInStorefront}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{data.filterableInStorefront && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!formErrors.storefrontSearchPosition}
|
||||
fullWidth
|
||||
helperText={getAttributeErrorMessage(
|
||||
formErrors.storefrontSearchPosition,
|
||||
intl
|
||||
)}
|
||||
name={
|
||||
"storefrontSearchPosition" as keyof AttributePageFormData
|
||||
}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Position in faceted navigation",
|
||||
description: "attribute position in storefront filters"
|
||||
})}
|
||||
value={data.storefrontSearchPosition}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{storefrontFacetedNavigationProperties && (
|
||||
<>
|
||||
<ControlledCheckbox
|
||||
name={"filterableInStorefront" as keyof FormData}
|
||||
label={intl.formatMessage(messages.filterableInStorefront)}
|
||||
checked={data.filterableInStorefront}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{data.filterableInStorefront && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
error={!!formErrors.storefrontSearchPosition}
|
||||
fullWidth
|
||||
helperText={getAttributeErrorMessage(
|
||||
formErrors.storefrontSearchPosition,
|
||||
intl
|
||||
)}
|
||||
name={
|
||||
"storefrontSearchPosition" as keyof AttributePageFormData
|
||||
}
|
||||
label={intl.formatMessage(messages.storefrontSearchPosition)}
|
||||
value={data.storefrontSearchPosition}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<FormSpacer />
|
||||
<ControlledSwitch
|
||||
name={"visibleInStorefront" as keyof FormData}
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage
|
||||
defaultMessage="Public"
|
||||
description="attribute visibility in storefront"
|
||||
/>
|
||||
<FormattedMessage {...messages.visibleInStorefront} />
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage defaultMessage="If enabled, attribute will be accessible to customers." />
|
||||
<FormattedMessage {...messages.visibleInStorefrontCaption} />
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
|
@ -136,14 +174,11 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
|
|||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{data.inputType !== AttributeInputTypeEnum.FILE && (
|
||||
{dashboardProperties && (
|
||||
<>
|
||||
<CardSpacer />
|
||||
<Typography variant="subtitle1">
|
||||
<FormattedMessage
|
||||
defaultMessage="Dashboard Properties"
|
||||
description="attribute properties regarding dashboard"
|
||||
/>
|
||||
<FormattedMessage {...messages.dashboardPropertiesTitle} />
|
||||
</Typography>
|
||||
<Hr />
|
||||
<CardSpacer />
|
||||
|
@ -151,12 +186,11 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
|
|||
name={"filterableInDashboard" as keyof FormData}
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage
|
||||
defaultMessage="Use in Filtering"
|
||||
description="use attribute in filtering"
|
||||
/>
|
||||
<FormattedMessage {...messages.filterableInDashboard} />
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage defaultMessage="If enabled, you’ll be able to use this attribute to filter products in product list." />
|
||||
<FormattedMessage
|
||||
{...messages.filterableInDashboardCaption}
|
||||
/>
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
|
@ -169,12 +203,9 @@ const AttributeProperties: React.FC<AttributePropertiesProps> = ({
|
|||
name={"availableInGrid" as keyof FormData}
|
||||
label={
|
||||
<>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add to Column Options"
|
||||
description="add attribute as column in product list table"
|
||||
/>
|
||||
<FormattedMessage {...messages.availableInGrid} />
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage defaultMessage="If enabled this attribute can be used as a column in product table." />
|
||||
<FormattedMessage {...messages.availableInGridCaption} />
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { AttributeList_attributes_edges_node } from "./types/AttributeList";
|
|||
export const attribute: AttributeDetailsFragment = {
|
||||
__typename: "Attribute" as "Attribute",
|
||||
availableInGrid: true,
|
||||
entityType: null,
|
||||
filterableInDashboard: false,
|
||||
filterableInStorefront: true,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZTo5",
|
||||
|
@ -33,6 +34,7 @@ export const attribute: AttributeDetailsFragment = {
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI0",
|
||||
name: "John Doe",
|
||||
reference: null,
|
||||
slug: "john-doe"
|
||||
},
|
||||
{
|
||||
|
@ -40,6 +42,7 @@ export const attribute: AttributeDetailsFragment = {
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI1",
|
||||
name: "Milionare Pirate",
|
||||
reference: null,
|
||||
slug: "milionare-pirate"
|
||||
}
|
||||
],
|
||||
|
@ -63,6 +66,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI0",
|
||||
name: "John Doe",
|
||||
reference: null,
|
||||
slug: "john-doe",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -72,6 +76,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI1",
|
||||
name: "Milionare Pirate",
|
||||
reference: null,
|
||||
slug: "milionare-pirate",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -95,6 +100,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE1",
|
||||
name: "100g",
|
||||
reference: null,
|
||||
slug: "100g",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -104,6 +110,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE2",
|
||||
name: "250g",
|
||||
reference: null,
|
||||
slug: "250g",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -113,6 +120,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE3",
|
||||
name: "500g",
|
||||
reference: null,
|
||||
slug: "500g",
|
||||
sortOrder: 2,
|
||||
value: ""
|
||||
|
@ -122,6 +130,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE4",
|
||||
name: "1kg",
|
||||
reference: null,
|
||||
slug: "1kg",
|
||||
sortOrder: 3,
|
||||
value: ""
|
||||
|
@ -145,6 +154,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjY=",
|
||||
name: "Saleor",
|
||||
reference: null,
|
||||
slug: "saleor",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -168,6 +178,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIx",
|
||||
name: "100g",
|
||||
reference: null,
|
||||
slug: "100g",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -177,6 +188,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIy",
|
||||
name: "250g",
|
||||
reference: null,
|
||||
slug: "250g",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -186,6 +198,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIz",
|
||||
name: "500g",
|
||||
reference: null,
|
||||
slug: "500g",
|
||||
sortOrder: 2,
|
||||
value: ""
|
||||
|
@ -209,6 +222,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEz",
|
||||
name: "Arabica",
|
||||
reference: null,
|
||||
slug: "arabica",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -218,6 +232,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE0",
|
||||
name: "Robusta",
|
||||
reference: null,
|
||||
slug: "robusta",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -241,6 +256,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM=",
|
||||
name: "Round",
|
||||
reference: null,
|
||||
slug: "round",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -250,6 +266,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjQ=",
|
||||
name: "V-Neck",
|
||||
reference: null,
|
||||
slug: "v-neck",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -259,6 +276,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjU=",
|
||||
name: "Polo",
|
||||
reference: null,
|
||||
slug: "polo",
|
||||
sortOrder: 2,
|
||||
value: ""
|
||||
|
@ -282,6 +300,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE=",
|
||||
name: "Blue",
|
||||
reference: null,
|
||||
slug: "blue",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -291,6 +310,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI=",
|
||||
name: "White",
|
||||
reference: null,
|
||||
slug: "white",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -314,6 +334,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMw",
|
||||
name: "Soft",
|
||||
reference: null,
|
||||
slug: "soft",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -323,6 +344,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMx",
|
||||
name: "Hard",
|
||||
reference: null,
|
||||
slug: "hard",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -332,6 +354,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMy",
|
||||
name: "Middle soft",
|
||||
reference: null,
|
||||
slug: "middle-soft",
|
||||
sortOrder: 2,
|
||||
value: ""
|
||||
|
@ -341,6 +364,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMz",
|
||||
name: "Middle hard",
|
||||
reference: null,
|
||||
slug: "middle-hard",
|
||||
sortOrder: 3,
|
||||
value: ""
|
||||
|
@ -350,6 +374,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM0",
|
||||
name: "Middle",
|
||||
reference: null,
|
||||
slug: "middle",
|
||||
sortOrder: 4,
|
||||
value: ""
|
||||
|
@ -359,6 +384,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM1",
|
||||
name: "Very hard",
|
||||
reference: null,
|
||||
slug: "very-hard",
|
||||
sortOrder: 5,
|
||||
value: ""
|
||||
|
@ -382,6 +408,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE5",
|
||||
name: "Sour",
|
||||
reference: null,
|
||||
slug: "sour",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -391,6 +418,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIw",
|
||||
name: "Sweet",
|
||||
reference: null,
|
||||
slug: "sweet",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -414,6 +442,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI4",
|
||||
name: "English",
|
||||
reference: null,
|
||||
slug: "english",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -423,6 +452,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI5",
|
||||
name: "Pirate",
|
||||
reference: null,
|
||||
slug: "pirate",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -446,6 +476,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI2",
|
||||
name: "Mirumee Press",
|
||||
reference: null,
|
||||
slug: "mirumee-press",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -455,6 +486,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI3",
|
||||
name: "Saleor Publishing",
|
||||
reference: null,
|
||||
slug: "saleor-publishing",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -478,6 +510,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjc=",
|
||||
name: "XS",
|
||||
reference: null,
|
||||
slug: "xs",
|
||||
sortOrder: 0,
|
||||
value: ""
|
||||
|
@ -487,6 +520,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjg=",
|
||||
name: "S",
|
||||
reference: null,
|
||||
slug: "s",
|
||||
sortOrder: 1,
|
||||
value: ""
|
||||
|
@ -496,6 +530,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjk=",
|
||||
name: "M",
|
||||
reference: null,
|
||||
slug: "m",
|
||||
sortOrder: 2,
|
||||
value: ""
|
||||
|
@ -505,6 +540,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEw",
|
||||
name: "L",
|
||||
reference: null,
|
||||
slug: "l",
|
||||
sortOrder: 3,
|
||||
value: ""
|
||||
|
@ -514,6 +550,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEx",
|
||||
name: "XL",
|
||||
reference: null,
|
||||
slug: "xl",
|
||||
sortOrder: 4,
|
||||
value: ""
|
||||
|
@ -523,6 +560,7 @@ export const attributes: Array<AttributeList_attributes_edges_node &
|
|||
file: null,
|
||||
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEy",
|
||||
name: "XXL",
|
||||
reference: null,
|
||||
slug: "xxl",
|
||||
sortOrder: 5,
|
||||
value: ""
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AttributeCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
import { AttributeCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: AttributeCreate
|
||||
|
@ -32,6 +32,7 @@ export interface AttributeCreate_attributeCreate_attribute_values {
|
|||
name: string | null;
|
||||
slug: string | null;
|
||||
file: AttributeCreate_attributeCreate_attribute_values_file | null;
|
||||
reference: string | null;
|
||||
}
|
||||
|
||||
export interface AttributeCreate_attributeCreate_attribute {
|
||||
|
@ -47,6 +48,7 @@ export interface AttributeCreate_attributeCreate_attribute {
|
|||
privateMetadata: (AttributeCreate_attributeCreate_attribute_privateMetadata | null)[];
|
||||
availableInGrid: boolean;
|
||||
inputType: AttributeInputTypeEnum | null;
|
||||
entityType: AttributeEntityTypeEnum | null;
|
||||
storefrontSearchPosition: number;
|
||||
valueRequired: boolean;
|
||||
values: (AttributeCreate_attributeCreate_attribute_values | null)[] | null;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AttributeTypeEnum, AttributeInputTypeEnum } from "./../../types/globalTypes";
|
||||
import { AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: AttributeDetails
|
||||
|
@ -32,6 +32,7 @@ export interface AttributeDetails_attribute_values {
|
|||
name: string | null;
|
||||
slug: string | null;
|
||||
file: AttributeDetails_attribute_values_file | null;
|
||||
reference: string | null;
|
||||
}
|
||||
|
||||
export interface AttributeDetails_attribute {
|
||||
|
@ -47,6 +48,7 @@ export interface AttributeDetails_attribute {
|
|||
privateMetadata: (AttributeDetails_attribute_privateMetadata | null)[];
|
||||
availableInGrid: boolean;
|
||||
inputType: AttributeInputTypeEnum | null;
|
||||
entityType: AttributeEntityTypeEnum | null;
|
||||
storefrontSearchPosition: number;
|
||||
valueRequired: boolean;
|
||||
values: (AttributeDetails_attribute_values | null)[] | null;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AttributeUpdateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
import { AttributeUpdateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: AttributeUpdate
|
||||
|
@ -32,6 +32,7 @@ export interface AttributeUpdate_attributeUpdate_attribute_values {
|
|||
name: string | null;
|
||||
slug: string | null;
|
||||
file: AttributeUpdate_attributeUpdate_attribute_values_file | null;
|
||||
reference: string | null;
|
||||
}
|
||||
|
||||
export interface AttributeUpdate_attributeUpdate_attribute {
|
||||
|
@ -47,6 +48,7 @@ export interface AttributeUpdate_attributeUpdate_attribute {
|
|||
privateMetadata: (AttributeUpdate_attributeUpdate_attribute_privateMetadata | null)[];
|
||||
availableInGrid: boolean;
|
||||
inputType: AttributeInputTypeEnum | null;
|
||||
entityType: AttributeEntityTypeEnum | null;
|
||||
storefrontSearchPosition: number;
|
||||
valueRequired: boolean;
|
||||
values: (AttributeUpdate_attributeUpdate_attribute_values | null)[] | null;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: AttributeValueCreate
|
||||
|
@ -32,6 +32,7 @@ export interface AttributeValueCreate_attributeValueCreate_attribute_values {
|
|||
name: string | null;
|
||||
slug: string | null;
|
||||
file: AttributeValueCreate_attributeValueCreate_attribute_values_file | null;
|
||||
reference: string | null;
|
||||
}
|
||||
|
||||
export interface AttributeValueCreate_attributeValueCreate_attribute {
|
||||
|
@ -47,6 +48,7 @@ export interface AttributeValueCreate_attributeValueCreate_attribute {
|
|||
privateMetadata: (AttributeValueCreate_attributeValueCreate_attribute_privateMetadata | null)[];
|
||||
availableInGrid: boolean;
|
||||
inputType: AttributeInputTypeEnum | null;
|
||||
entityType: AttributeEntityTypeEnum | null;
|
||||
storefrontSearchPosition: number;
|
||||
valueRequired: boolean;
|
||||
values: (AttributeValueCreate_attributeValueCreate_attribute_values | null)[] | null;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
import { AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: AttributeValueDelete
|
||||
|
@ -32,6 +32,7 @@ export interface AttributeValueDelete_attributeValueDelete_attribute_values {
|
|||
name: string | null;
|
||||
slug: string | null;
|
||||
file: AttributeValueDelete_attributeValueDelete_attribute_values_file | null;
|
||||
reference: string | null;
|
||||
}
|
||||
|
||||
export interface AttributeValueDelete_attributeValueDelete_attribute {
|
||||
|
@ -47,6 +48,7 @@ export interface AttributeValueDelete_attributeValueDelete_attribute {
|
|||
privateMetadata: (AttributeValueDelete_attributeValueDelete_attribute_privateMetadata | null)[];
|
||||
availableInGrid: boolean;
|
||||
inputType: AttributeInputTypeEnum | null;
|
||||
entityType: AttributeEntityTypeEnum | null;
|
||||
storefrontSearchPosition: number;
|
||||
valueRequired: boolean;
|
||||
values: (AttributeValueDelete_attributeValueDelete_attribute_values | null)[] | null;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable */
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
import { AttributeValueCreateInput, AttributeTypeEnum, AttributeInputTypeEnum, AttributeEntityTypeEnum, AttributeErrorCode } from "./../../types/globalTypes";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL mutation operation: AttributeValueUpdate
|
||||
|
@ -32,6 +32,7 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute_values {
|
|||
name: string | null;
|
||||
slug: string | null;
|
||||
file: AttributeValueUpdate_attributeValueUpdate_attribute_values_file | null;
|
||||
reference: string | null;
|
||||
}
|
||||
|
||||
export interface AttributeValueUpdate_attributeValueUpdate_attribute {
|
||||
|
@ -47,6 +48,7 @@ export interface AttributeValueUpdate_attributeValueUpdate_attribute {
|
|||
privateMetadata: (AttributeValueUpdate_attributeValueUpdate_attribute_privateMetadata | null)[];
|
||||
availableInGrid: boolean;
|
||||
inputType: AttributeInputTypeEnum | null;
|
||||
entityType: AttributeEntityTypeEnum | null;
|
||||
storefrontSearchPosition: number;
|
||||
valueRequired: boolean;
|
||||
values: (AttributeValueUpdate_attributeValueUpdate_attribute_values | null)[] | null;
|
||||
|
|
|
@ -1,17 +1,88 @@
|
|||
import {
|
||||
AttributeInput,
|
||||
AttributeInputData
|
||||
} from "@saleor/components/Attributes";
|
||||
import { FileUpload } from "@saleor/files/types/FileUpload";
|
||||
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
|
||||
import { SelectedVariantAttributeFragment } from "@saleor/fragments/types/SelectedVariantAttributeFragment";
|
||||
import { UploadErrorFragment } from "@saleor/fragments/types/UploadErrorFragment";
|
||||
import { FormsetData } from "@saleor/hooks/useFormset";
|
||||
import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails";
|
||||
import { ProductDetails_product_attributes } from "@saleor/products/types/ProductDetails";
|
||||
import { SearchPages_search_edges_node } from "@saleor/searches/types/SearchPages";
|
||||
import { SearchProducts_search_edges_node } from "@saleor/searches/types/SearchProducts";
|
||||
import {
|
||||
AttributeEntityTypeEnum,
|
||||
AttributeInputTypeEnum,
|
||||
AttributeValueInput
|
||||
} from "@saleor/types/globalTypes";
|
||||
import { mapNodeToChoice, mapPagesToChoices } from "@saleor/utils/maps";
|
||||
import { MutationFetchResult } from "react-apollo";
|
||||
|
||||
import { AttributePageFormData } from "../components/AttributePage";
|
||||
import { AttributeValueEditDialogFormData } from "../components/AttributeValueEditDialog";
|
||||
import { AttributeValueDelete } from "../types/AttributeValueDelete";
|
||||
|
||||
export const ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES = [
|
||||
AttributeInputTypeEnum.DROPDOWN,
|
||||
AttributeInputTypeEnum.MULTISELECT
|
||||
];
|
||||
|
||||
export interface AttributeReference {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function getSimpleAttributeData(
|
||||
data: AttributePageFormData,
|
||||
values: AttributeValueEditDialogFormData[]
|
||||
) {
|
||||
return {
|
||||
...data,
|
||||
metadata: undefined,
|
||||
privateMetadata: undefined,
|
||||
storefrontSearchPosition: parseInt(data.storefrontSearchPosition, 10),
|
||||
values: values.map(value => ({
|
||||
name: value.name
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function getFileOrReferenceAttributeData(
|
||||
data: AttributePageFormData,
|
||||
values: AttributeValueEditDialogFormData[]
|
||||
) {
|
||||
return {
|
||||
...getSimpleAttributeData(data, values),
|
||||
availableInGrid: undefined,
|
||||
filterableInDashboard: undefined,
|
||||
filterableInStorefront: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function getAttributeData(
|
||||
data: AttributePageFormData,
|
||||
values: AttributeValueEditDialogFormData[]
|
||||
) {
|
||||
if (ATTRIBUTE_TYPES_WITH_DEDICATED_VALUES.includes(data.inputType)) {
|
||||
return getSimpleAttributeData(data, values);
|
||||
} else {
|
||||
return getFileOrReferenceAttributeData(data, values);
|
||||
}
|
||||
}
|
||||
|
||||
export function getSelectedAttributeValues(
|
||||
attribute:
|
||||
| PageDetails_page_attributes
|
||||
| ProductDetails_product_attributes
|
||||
| SelectedVariantAttributeFragment
|
||||
) {
|
||||
if (attribute.attribute.inputType === AttributeInputTypeEnum.REFERENCE) {
|
||||
return attribute.values.map(value => value.reference);
|
||||
}
|
||||
return attribute.values.map(value => value.slug);
|
||||
}
|
||||
|
||||
export const isFileValueUnused = (
|
||||
attributesWithNewFileValue: FormsetData<null, File>,
|
||||
existingAttribute:
|
||||
|
@ -54,6 +125,18 @@ export const mergeAttributeValueDeleteErrors = (
|
|||
return errors;
|
||||
}, []);
|
||||
|
||||
export const mergeAttributeValues = (
|
||||
attributeId: string,
|
||||
attributeValues: string[],
|
||||
attributes: FormsetData<AttributeInputData, string[]>
|
||||
) => {
|
||||
const attribute = attributes.find(attribute => attribute.id === attributeId);
|
||||
|
||||
return attribute.value
|
||||
? [...attribute.value, ...attributeValues]
|
||||
: attributeValues;
|
||||
};
|
||||
|
||||
export const getFileValuesToUploadFromAttributes = (
|
||||
attributesWithNewFileValue: FormsetData<null, File>
|
||||
) => attributesWithNewFileValue.filter(fileAttribute => !!fileAttribute.value);
|
||||
|
@ -104,3 +187,131 @@ export const getAttributesAfterFileAttributesUpdate = (
|
|||
|
||||
return uploadedFileAttributes.concat(removedFileAttributes);
|
||||
};
|
||||
|
||||
export const getFileAttributeDisplayData = (
|
||||
attribute: AttributeInput,
|
||||
attributesWithNewFileValue: FormsetData<null, File>
|
||||
) => {
|
||||
const attributeWithNewFileValue = attributesWithNewFileValue.find(
|
||||
attributeWithNewFile => attribute.id === attributeWithNewFile.id
|
||||
);
|
||||
|
||||
if (attributeWithNewFileValue) {
|
||||
return {
|
||||
...attribute,
|
||||
value: attributeWithNewFileValue?.value?.name
|
||||
? [attributeWithNewFileValue.value.name]
|
||||
: []
|
||||
};
|
||||
}
|
||||
return attribute;
|
||||
};
|
||||
|
||||
export const getPageReferenceAttributeDisplayData = (
|
||||
attribute: AttributeInput,
|
||||
referencePages: SearchPages_search_edges_node[]
|
||||
) => ({
|
||||
...attribute,
|
||||
data: {
|
||||
...attribute.data,
|
||||
references:
|
||||
referencePages?.length > 0 && attribute.value?.length > 0
|
||||
? mapPagesToChoices(
|
||||
attribute.value.map(value => {
|
||||
const reference = referencePages.find(
|
||||
reference => reference.id === value
|
||||
);
|
||||
return { ...reference };
|
||||
})
|
||||
)
|
||||
: []
|
||||
}
|
||||
});
|
||||
|
||||
export const getProductReferenceAttributeDisplayData = (
|
||||
attribute: AttributeInput,
|
||||
referenceProducts: SearchProducts_search_edges_node[]
|
||||
) => ({
|
||||
...attribute,
|
||||
data: {
|
||||
...attribute.data,
|
||||
references:
|
||||
referenceProducts?.length > 0 && attribute.value?.length > 0
|
||||
? mapNodeToChoice(
|
||||
attribute.value.map(value => {
|
||||
const reference = referenceProducts.find(
|
||||
reference => reference.id === value
|
||||
);
|
||||
return { ...reference };
|
||||
})
|
||||
)
|
||||
: []
|
||||
}
|
||||
});
|
||||
|
||||
export const getReferenceAttributeDisplayData = (
|
||||
attribute: AttributeInput,
|
||||
referencePages: SearchPages_search_edges_node[],
|
||||
referenceProducts: SearchProducts_search_edges_node[]
|
||||
) => {
|
||||
if (attribute.data.entityType === AttributeEntityTypeEnum.PAGE) {
|
||||
return getPageReferenceAttributeDisplayData(attribute, referencePages);
|
||||
} else if (attribute.data.entityType === AttributeEntityTypeEnum.PRODUCT) {
|
||||
return getProductReferenceAttributeDisplayData(
|
||||
attribute,
|
||||
referenceProducts
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getAttributesDisplayData = (
|
||||
attributes: AttributeInput[],
|
||||
attributesWithNewFileValue: FormsetData<null, File>,
|
||||
referencePages: SearchPages_search_edges_node[],
|
||||
referenceProducts: SearchProducts_search_edges_node[]
|
||||
) =>
|
||||
attributes.map(attribute => {
|
||||
if (attribute.data.inputType === AttributeInputTypeEnum.REFERENCE) {
|
||||
return getReferenceAttributeDisplayData(
|
||||
attribute,
|
||||
referencePages,
|
||||
referenceProducts
|
||||
);
|
||||
}
|
||||
if (attribute.data.inputType === AttributeInputTypeEnum.FILE) {
|
||||
return getFileAttributeDisplayData(attribute, attributesWithNewFileValue);
|
||||
}
|
||||
|
||||
return attribute;
|
||||
});
|
||||
|
||||
export const getSelectedReferencesFromAttribute = <
|
||||
Node extends SearchPages_search_edges_node | SearchProducts_search_edges_node
|
||||
>(
|
||||
attribute?: AttributeInput,
|
||||
references?: Node[]
|
||||
) =>
|
||||
references?.filter(
|
||||
value =>
|
||||
!attribute?.value?.some(selectedValue => selectedValue === value.id)
|
||||
) || [];
|
||||
|
||||
export const getAttributeValuesFromReferences = (
|
||||
attributeId: string,
|
||||
attributes?: AttributeInput[],
|
||||
referencePages?: SearchPages_search_edges_node[],
|
||||
referenceProducts?: SearchProducts_search_edges_node[]
|
||||
) => {
|
||||
const attribute = attributes?.find(attribute => attribute.id === attributeId);
|
||||
|
||||
if (attribute?.data?.entityType === AttributeEntityTypeEnum.PAGE) {
|
||||
return mapPagesToChoices(
|
||||
getSelectedReferencesFromAttribute(attribute, referencePages)
|
||||
);
|
||||
} else if (attribute?.data?.entityType === AttributeEntityTypeEnum.PRODUCT) {
|
||||
return mapNodeToChoice(
|
||||
getSelectedReferencesFromAttribute(attribute, referenceProducts)
|
||||
);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { createAttributeMultiChangeHandler } from "@saleor/attributes/utils/handlers";
|
||||
import { AttributeInputData } from "@saleor/components/Attributes";
|
||||
import { FormsetData } from "@saleor/hooks/useFormset";
|
||||
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
|
||||
|
||||
import { createAttributeMultiChangeHandler } from "./handlers";
|
||||
|
||||
const attributes: FormsetData<AttributeInputData, string[]> = [
|
||||
{
|
||||
data: {
|
||||
|
@ -15,6 +14,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
|
|||
file: null,
|
||||
id: "attrv-1",
|
||||
name: "Attribute 1 Value 1",
|
||||
reference: null,
|
||||
slug: "attr-1-v-1"
|
||||
}
|
||||
]
|
||||
|
@ -33,6 +33,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
|
|||
file: null,
|
||||
id: "attrv-2",
|
||||
name: "Attribute 2 Value 1",
|
||||
reference: null,
|
||||
slug: "attr-2-v-1"
|
||||
},
|
||||
{
|
||||
|
@ -40,6 +41,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
|
|||
file: null,
|
||||
id: "attrv-3",
|
||||
name: "Attribute 2 Value 2",
|
||||
reference: null,
|
||||
slug: "attr-2-v-2"
|
||||
},
|
||||
{
|
||||
|
@ -47,6 +49,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
|
|||
file: null,
|
||||
id: "attrv-4",
|
||||
name: "Attribute 2 Value 3",
|
||||
reference: null,
|
||||
slug: "attr-2-v-3"
|
||||
}
|
||||
]
|
||||
|
@ -69,6 +72,7 @@ const attributes: FormsetData<AttributeInputData, string[]> = [
|
|||
},
|
||||
id: "gdghdgdhkkdae",
|
||||
name: "File First Value",
|
||||
reference: null,
|
||||
slug: "file-first-value"
|
||||
}
|
||||
]
|
|
@ -1,14 +1,24 @@
|
|||
import { AttributeInput } from "@saleor/components/Attributes";
|
||||
import {
|
||||
AttributeInput,
|
||||
AttributeInputData
|
||||
} from "@saleor/components/Attributes";
|
||||
import {
|
||||
FileUpload,
|
||||
FileUploadVariables
|
||||
} from "@saleor/files/types/FileUpload";
|
||||
import { FormsetData } from "@saleor/hooks/useFormset";
|
||||
import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails";
|
||||
import {
|
||||
FormsetAtomicData,
|
||||
FormsetChange,
|
||||
FormsetData
|
||||
} from "@saleor/hooks/useFormset";
|
||||
import { PageDetails_page_attributes } from "@saleor/pages/types/PageDetails";
|
||||
import { FetchMoreProps, ReorderEvent } from "@saleor/types";
|
||||
import {
|
||||
AttributeEntityTypeEnum,
|
||||
AttributeInputTypeEnum,
|
||||
AttributeValueInput
|
||||
} from "@saleor/types/globalTypes";
|
||||
import { move, toggle } from "@saleor/utils/lists";
|
||||
import { MutationFetchResult } from "react-apollo";
|
||||
|
||||
import {
|
||||
|
@ -17,6 +27,149 @@ import {
|
|||
} from "../types/AttributeValueDelete";
|
||||
import { getFileValuesToUploadFromAttributes, isFileValueUnused } from "./data";
|
||||
|
||||
export function createAttributeChangeHandler(
|
||||
changeAttributeData: FormsetChange<string[]>,
|
||||
triggerChange: () => void
|
||||
): FormsetChange<string> {
|
||||
return (attributeId: string, value: string) => {
|
||||
triggerChange();
|
||||
changeAttributeData(attributeId, value === "" ? [] : [value]);
|
||||
};
|
||||
}
|
||||
|
||||
export function createAttributeMultiChangeHandler(
|
||||
changeAttributeData: FormsetChange<string[]>,
|
||||
attributes: FormsetData<AttributeInputData, string[]>,
|
||||
triggerChange: () => void
|
||||
): FormsetChange<string> {
|
||||
return (attributeId: string, value: string) => {
|
||||
const attribute = attributes.find(
|
||||
attribute => attribute.id === attributeId
|
||||
);
|
||||
|
||||
const newAttributeValues = toggle(
|
||||
value,
|
||||
attribute.value,
|
||||
(a, b) => a === b
|
||||
);
|
||||
|
||||
triggerChange();
|
||||
changeAttributeData(attributeId, newAttributeValues);
|
||||
};
|
||||
}
|
||||
|
||||
export function createAttributeReferenceChangeHandler(
|
||||
changeAttributeData: FormsetChange<string[]>,
|
||||
triggerChange: () => void
|
||||
): FormsetChange<string[]> {
|
||||
return (attributeId: string, values: string[]) => {
|
||||
changeAttributeData(attributeId, values);
|
||||
triggerChange();
|
||||
};
|
||||
}
|
||||
|
||||
export function createFetchReferencesHandler(
|
||||
attributes: FormsetData<AttributeInputData, string[]>,
|
||||
assignReferencesAttributeId: string,
|
||||
fetchReferencePages?: (data: string) => void,
|
||||
fetchReferenceProducts?: (data: string) => void
|
||||
) {
|
||||
return (value: string) => {
|
||||
const attribute = attributes?.find(
|
||||
attribute => attribute.id === assignReferencesAttributeId
|
||||
);
|
||||
|
||||
if (!attribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
attribute.data.entityType === AttributeEntityTypeEnum.PAGE &&
|
||||
fetchReferencePages
|
||||
) {
|
||||
fetchReferencePages(value);
|
||||
} else if (
|
||||
attribute.data.entityType === AttributeEntityTypeEnum.PRODUCT &&
|
||||
fetchReferenceProducts
|
||||
) {
|
||||
fetchReferenceProducts(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createFetchMoreReferencesHandler(
|
||||
attributes: FormsetData<AttributeInputData, string[]>,
|
||||
assignReferencesAttributeId: string,
|
||||
fetchMoreReferencePages?: FetchMoreProps,
|
||||
fetchMoreReferenceProducts?: FetchMoreProps
|
||||
) {
|
||||
const attribute = attributes?.find(
|
||||
attribute => attribute.id === assignReferencesAttributeId
|
||||
);
|
||||
|
||||
if (!attribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attribute.data.entityType === AttributeEntityTypeEnum.PAGE) {
|
||||
return fetchMoreReferencePages;
|
||||
} else if (attribute.data.entityType === AttributeEntityTypeEnum.PRODUCT) {
|
||||
return fetchMoreReferenceProducts;
|
||||
}
|
||||
}
|
||||
|
||||
export function createAttributeFileChangeHandler(
|
||||
changeAttributeData: FormsetChange<string[]>,
|
||||
attributesWithNewFileValue: FormsetData<FormsetData<null, File>>,
|
||||
addAttributeNewFileValue: (data: FormsetAtomicData<null, File>) => void,
|
||||
changeAttributeNewFileValue: FormsetChange<File>,
|
||||
triggerChange: () => void
|
||||
): FormsetChange<File> {
|
||||
return (attributeId: string, value: File) => {
|
||||
triggerChange();
|
||||
|
||||
const newFileValueAssigned = attributesWithNewFileValue.find(
|
||||
attribute => attribute.id === attributeId
|
||||
);
|
||||
|
||||
if (newFileValueAssigned) {
|
||||
changeAttributeNewFileValue(attributeId, value);
|
||||
} else {
|
||||
addAttributeNewFileValue({
|
||||
data: null,
|
||||
id: attributeId,
|
||||
label: null,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
changeAttributeData(attributeId, value ? [value.name] : []);
|
||||
};
|
||||
}
|
||||
|
||||
export function createAttributeValueReorderHandler(
|
||||
changeAttributeData: FormsetChange<string[]>,
|
||||
attributes: FormsetData<AttributeInputData, string[]>,
|
||||
triggerChange: () => void
|
||||
): FormsetChange<ReorderEvent> {
|
||||
return (attributeId: string, reorder: ReorderEvent) => {
|
||||
triggerChange();
|
||||
|
||||
const attribute = attributes.find(
|
||||
attribute => attribute.id === attributeId
|
||||
);
|
||||
|
||||
const reorderedValues = move(
|
||||
attribute.value[reorder.oldIndex],
|
||||
attribute.value,
|
||||
(a, b) => a === b,
|
||||
reorder.newIndex
|
||||
);
|
||||
|
||||
changeAttributeData(attributeId, reorderedValues);
|
||||
};
|
||||
}
|
||||
|
||||
interface AttributesArgs {
|
||||
attributes: AttributeInput[];
|
||||
updatedFileAttributes: AttributeValueInput[];
|
||||
|
@ -44,6 +197,12 @@ export const prepareAttributesInput = ({
|
|||
id: attribute.id
|
||||
};
|
||||
}
|
||||
if (attribute.data.inputType === AttributeInputTypeEnum.REFERENCE) {
|
||||
return {
|
||||
id: attribute.id,
|
||||
references: attribute.value
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: attribute.id,
|
||||
values: attribute.value[0] === "" ? [] : attribute.value
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { getAttributeData } from "@saleor/attributes/utils/data";
|
||||
import { AttributeErrorFragment } from "@saleor/fragments/types/AttributeErrorFragment";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import { getStringOrPlaceholder } from "@saleor/misc";
|
||||
import { ReorderEvent } from "@saleor/types";
|
||||
import {
|
||||
AttributeErrorCode,
|
||||
AttributeInputTypeEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
import { AttributeErrorCode } from "@saleor/types/globalTypes";
|
||||
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
|
||||
import createMetadataCreateHandler from "@saleor/utils/handlers/metadataCreateHandler";
|
||||
import {
|
||||
|
@ -57,33 +55,6 @@ function areValuesEqual(
|
|||
return a.name === b.name;
|
||||
}
|
||||
|
||||
function getSimpleAttributeData(
|
||||
data: AttributePageFormData,
|
||||
values: AttributeValueEditDialogFormData[]
|
||||
) {
|
||||
return {
|
||||
...data,
|
||||
metadata: undefined,
|
||||
privateMetadata: undefined,
|
||||
storefrontSearchPosition: parseInt(data.storefrontSearchPosition, 10),
|
||||
values: values.map(value => ({
|
||||
name: value.name
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function getFileAttributeData(
|
||||
data: AttributePageFormData,
|
||||
values: AttributeValueEditDialogFormData[]
|
||||
) {
|
||||
return {
|
||||
...getSimpleAttributeData(data, values),
|
||||
availableInGrid: undefined,
|
||||
filterableInDashboard: undefined,
|
||||
filterableInStorefront: undefined
|
||||
};
|
||||
}
|
||||
|
||||
const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
|
||||
const navigate = useNavigator();
|
||||
const notify = useNotifier();
|
||||
|
@ -145,10 +116,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
|
|||
setValues(move(values[oldIndex], values, areValuesEqual, newIndex));
|
||||
|
||||
const handleCreate = async (data: AttributePageFormData) => {
|
||||
const input =
|
||||
data.inputType === AttributeInputTypeEnum.FILE
|
||||
? getFileAttributeData(data, values)
|
||||
: getSimpleAttributeData(data, values);
|
||||
const input = getAttributeData(data, values);
|
||||
|
||||
const result = await attributeCreate({
|
||||
variables: {
|
||||
|
@ -190,6 +158,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
|
|||
__typename: "AttributeValue" as "AttributeValue",
|
||||
file: null,
|
||||
id: valueIndex.toString(),
|
||||
reference: null,
|
||||
slug: slugify(value.name).toLowerCase(),
|
||||
sortOrder: valueIndex,
|
||||
value: null,
|
||||
|
|
|
@ -178,6 +178,7 @@ const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
|
|||
const handleUpdate = async (data: AttributePageFormData) => {
|
||||
const input = {
|
||||
...data,
|
||||
entityType: undefined,
|
||||
inputType: undefined,
|
||||
metadata: undefined,
|
||||
privateMetadata: undefined,
|
||||
|
|
|
@ -2,7 +2,7 @@ import setupApi from "@test/api";
|
|||
import { act, renderHook } from "@testing-library/react-hooks";
|
||||
import ApolloClient from "apollo-client";
|
||||
|
||||
import { useAuthProvider } from "./AuthProvider";
|
||||
import { useAuthProvider } from "./hooks/useAuthProvider";
|
||||
import { getTokens, setAuthToken } from "./utils";
|
||||
|
||||
const apolloClient = setupApi();
|
||||
|
@ -14,18 +14,23 @@ function renderAuthProvider(apolloClient: ApolloClient<any>) {
|
|||
const notify = jest.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAuthProvider(intl as any, notify, apolloClient)
|
||||
useAuthProvider({ apolloClient, intl: intl as any, notify })
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
const adminCredentials = {
|
||||
email: "admin@example.com",
|
||||
password: "admin",
|
||||
token: null
|
||||
};
|
||||
|
||||
const nonStaffUserCredentials = {
|
||||
email: "client@example.com",
|
||||
password: "password"
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
@ -36,10 +41,10 @@ describe("User", () => {
|
|||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(() =>
|
||||
hook.current.login(credentials.email, credentials.password)
|
||||
hook.current.login(adminCredentials.email, adminCredentials.password)
|
||||
);
|
||||
expect(hook.current.userContext.email).toBe(credentials.email);
|
||||
credentials.token = getTokens().auth;
|
||||
expect(hook.current.user.email).toBe(adminCredentials.email);
|
||||
adminCredentials.token = getTokens().auth;
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -48,19 +53,33 @@ describe("User", () => {
|
|||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(() =>
|
||||
hook.current.login(credentials.email, "NotAValidPassword123!")
|
||||
hook.current.login(adminCredentials.email, "NotAValidPassword123!")
|
||||
);
|
||||
expect(hook.current.userContext).toBe(null);
|
||||
expect(hook.current.user).toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("will not be logged in if is non-staff", async done => {
|
||||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(() =>
|
||||
hook.current.login(
|
||||
nonStaffUserCredentials.email,
|
||||
nonStaffUserCredentials.password
|
||||
)
|
||||
);
|
||||
expect(hook.current.user).toBe(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("will be logged if has valid token", async done => {
|
||||
setAuthToken(credentials.token, false);
|
||||
setAuthToken(adminCredentials.token, false);
|
||||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(() => hook.current.autologinPromise.current);
|
||||
expect(hook.current.userContext.email).toBe(credentials.email);
|
||||
expect(hook.current.user.email).toBe(adminCredentials.email);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -70,7 +89,7 @@ describe("User", () => {
|
|||
const hook = renderAuthProvider(apolloClient);
|
||||
|
||||
await act(() => hook.current.autologinPromise.current);
|
||||
expect(hook.current.userContext).toBe(undefined);
|
||||
expect(hook.current.user).toBe(undefined);
|
||||
|
||||
done();
|
||||
});
|
||||
|
|
|
@ -1,179 +1,11 @@
|
|||
import { IMessageContext } from "@saleor/components/messages";
|
||||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { User } from "@saleor/fragments/types/User";
|
||||
import useNotifier from "@saleor/hooks/useNotifier";
|
||||
import { getMutationStatus } from "@saleor/misc";
|
||||
import {
|
||||
isSupported as isCredentialsManagementAPISupported,
|
||||
login as loginWithCredentialsManagementAPI,
|
||||
saveCredentials
|
||||
} from "@saleor/utils/credentialsManagement";
|
||||
import ApolloClient from "apollo-client";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { useApolloClient, useMutation } from "react-apollo";
|
||||
import { IntlShape, useIntl } from "react-intl";
|
||||
import React, { useContext } from "react";
|
||||
import { useApolloClient } from "react-apollo";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { UserContext } from "./";
|
||||
import {
|
||||
tokenAuthMutation,
|
||||
tokenRefreshMutation,
|
||||
tokenVerifyMutation
|
||||
} from "./mutations";
|
||||
import { RefreshToken, RefreshTokenVariables } from "./types/RefreshToken";
|
||||
import { TokenAuth, TokenAuthVariables } from "./types/TokenAuth";
|
||||
import { VerifyToken, VerifyTokenVariables } from "./types/VerifyToken";
|
||||
import {
|
||||
displayDemoMessage,
|
||||
getTokens,
|
||||
removeTokens,
|
||||
setAuthToken,
|
||||
setTokens
|
||||
} from "./utils";
|
||||
|
||||
const persistToken = false;
|
||||
|
||||
export function useAuthProvider(
|
||||
intl: IntlShape,
|
||||
notify: IMessageContext,
|
||||
apolloClient: ApolloClient<any>
|
||||
) {
|
||||
const [userContext, setUserContext] = useState<undefined | User>(undefined);
|
||||
const autologinPromise = useRef<Promise<any>>();
|
||||
const refreshPromise = useRef<Promise<boolean>>();
|
||||
|
||||
const logout = () => {
|
||||
setUserContext(undefined);
|
||||
if (isCredentialsManagementAPISupported) {
|
||||
navigator.credentials.preventSilentAccess();
|
||||
}
|
||||
removeTokens();
|
||||
};
|
||||
|
||||
const [tokenAuth, tokenAuthResult] = useMutation<
|
||||
TokenAuth,
|
||||
TokenAuthVariables
|
||||
>(tokenAuthMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: result => {
|
||||
if (result.tokenCreate.errors.length > 0) {
|
||||
logout();
|
||||
}
|
||||
|
||||
const user = result.tokenCreate.user;
|
||||
|
||||
// FIXME: Now we set state also when auth fails and returned user is
|
||||
// `null`, because the LoginView uses this `null` to display error.
|
||||
setUserContext(user);
|
||||
if (user) {
|
||||
setTokens(
|
||||
result.tokenCreate.token,
|
||||
result.tokenCreate.csrfToken,
|
||||
persistToken
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
const [tokenRefresh] = useMutation<RefreshToken, RefreshTokenVariables>(
|
||||
tokenRefreshMutation,
|
||||
{
|
||||
client: apolloClient,
|
||||
onError: logout
|
||||
}
|
||||
);
|
||||
const [tokenVerify, tokenVerifyResult] = useMutation<
|
||||
VerifyToken,
|
||||
VerifyTokenVariables
|
||||
>(tokenVerifyMutation, {
|
||||
client: apolloClient,
|
||||
onCompleted: result => {
|
||||
if (result.tokenVerify === null) {
|
||||
logout();
|
||||
} else {
|
||||
const user = result.tokenVerify?.user;
|
||||
|
||||
if (!!user) {
|
||||
setUserContext(user);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: logout
|
||||
});
|
||||
|
||||
const tokenAuthOpts = {
|
||||
...tokenAuthResult,
|
||||
status: getMutationStatus(tokenAuthResult)
|
||||
};
|
||||
const tokenVerifyOpts = {
|
||||
...tokenVerifyResult,
|
||||
status: getMutationStatus(tokenVerifyResult)
|
||||
};
|
||||
|
||||
const onLogin = () => {
|
||||
if (DEMO_MODE) {
|
||||
displayDemoMessage(intl, notify);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const token = getTokens().auth;
|
||||
if (!!token && !userContext) {
|
||||
autologinPromise.current = tokenVerify({ variables: { token } });
|
||||
} else {
|
||||
autologinPromise.current = loginWithCredentialsManagementAPI(login);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
const result = await tokenAuth({ variables: { email, password } });
|
||||
|
||||
if (result && !result.data.tokenCreate.errors.length) {
|
||||
if (!!onLogin) {
|
||||
onLogin();
|
||||
}
|
||||
saveCredentials(result.data.tokenCreate.user, password);
|
||||
|
||||
return result.data.tokenCreate.user;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const loginByToken = (auth: string, refresh: string, user: User) => {
|
||||
setUserContext(user);
|
||||
setTokens(auth, refresh, persistToken);
|
||||
};
|
||||
|
||||
const refreshToken = (): Promise<boolean> => {
|
||||
if (!!refreshPromise.current) {
|
||||
return refreshPromise.current;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const token = getTokens().refresh;
|
||||
|
||||
return tokenRefresh({ variables: { token } }).then(refreshData => {
|
||||
if (!!refreshData.data.tokenRefresh?.token) {
|
||||
setAuthToken(refreshData.data.tokenRefresh.token, persistToken);
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
return resolve(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
autologinPromise,
|
||||
login,
|
||||
loginByToken,
|
||||
logout,
|
||||
refreshToken,
|
||||
tokenAuthOpts,
|
||||
tokenVerifyOpts,
|
||||
userContext
|
||||
};
|
||||
}
|
||||
import { useAuthProvider } from "./hooks/useAuthProvider";
|
||||
import { getTokens } from "./utils";
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
@ -184,30 +16,10 @@ const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|||
const intl = useIntl();
|
||||
const notify = useNotifier();
|
||||
|
||||
const {
|
||||
login,
|
||||
loginByToken,
|
||||
logout,
|
||||
tokenAuthOpts,
|
||||
refreshToken,
|
||||
tokenVerifyOpts,
|
||||
userContext
|
||||
} = useAuthProvider(intl, notify, apolloClient);
|
||||
const authProvider = useAuthProvider({ apolloClient, intl, notify });
|
||||
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
login,
|
||||
loginByToken,
|
||||
logout,
|
||||
tokenAuthLoading: tokenAuthOpts.loading,
|
||||
tokenRefresh: refreshToken,
|
||||
tokenVerifyLoading: tokenVerifyOpts.loading,
|
||||
user: userContext
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
<UserContext.Provider value={authProvider}>{children}</UserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import LoginLoading from "../../../auth/components/LoginLoading";
|
||||
import Decorator from "../../Decorator";
|
||||
import LoginLoading from ".";
|
||||
|
||||
storiesOf("Views / Authentication / Verifying remembered user", module)
|
||||
.addDecorator(Decorator)
|
|
@ -1,14 +1,24 @@
|
|||
import { Omit } from "@material-ui/core";
|
||||
import CardDecorator from "@saleor/storybook/CardDecorator";
|
||||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import LoginPage, { LoginCardProps } from "../../../auth/components/LoginPage";
|
||||
import CardDecorator from "../../CardDecorator";
|
||||
import Decorator from "../../Decorator";
|
||||
|
||||
const props: Omit<LoginCardProps, "classes"> = {
|
||||
disableLoginButton: true,
|
||||
disabled: false,
|
||||
error: false,
|
||||
externalAuthentications: [
|
||||
{
|
||||
__typename: "ExternalAuthentication",
|
||||
id: "auth.plugin.example",
|
||||
name: "Example auth plugin"
|
||||
}
|
||||
],
|
||||
externalError: false,
|
||||
loading: false,
|
||||
onExternalAuthentication: () => undefined,
|
||||
onPasswordRecovery: undefined,
|
||||
onSubmit: () => undefined
|
||||
};
|
||||
|
@ -18,4 +28,5 @@ storiesOf("Views / Authentication / Log in", module)
|
|||
.addDecorator(Decorator)
|
||||
.add("default", () => <LoginPage {...props} />)
|
||||
.add("error", () => <LoginPage {...props} error={true} />)
|
||||
.add("loading", () => <LoginPage {...props} disableLoginButton={true} />);
|
||||
.add("disabled", () => <LoginPage {...props} disabled={true} />)
|
||||
.add("loading", () => <LoginPage {...props} loading={true} />);
|
|
@ -1,18 +1,17 @@
|
|||
import Button from "@material-ui/core/Button";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Form from "@saleor/components/Form";
|
||||
import { AvailableExternalAuthentications_shop_availableExternalAuthentications } from "@saleor/auth/types/AvailableExternalAuthentications";
|
||||
import { FormSpacer } from "@saleor/components/FormSpacer";
|
||||
import { DEMO_MODE } from "@saleor/config";
|
||||
import { SubmitPromise } from "@saleor/hooks/useForm";
|
||||
import { commonMessages } from "@saleor/intl";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
export interface FormData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
import LoginForm, { LoginFormData } from "./form";
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
|
@ -23,7 +22,13 @@ const useStyles = makeStyles(
|
|||
link: {
|
||||
color: theme.palette.primary.main,
|
||||
cursor: "pointer",
|
||||
textAlign: "center"
|
||||
textDecoration: "underline"
|
||||
},
|
||||
loading: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
justifyContent: "center"
|
||||
},
|
||||
loginButton: {
|
||||
width: 140
|
||||
|
@ -43,27 +48,40 @@ const useStyles = makeStyles(
|
|||
|
||||
export interface LoginCardProps {
|
||||
error: boolean;
|
||||
disableLoginButton: boolean;
|
||||
externalError: boolean;
|
||||
disabled: boolean;
|
||||
loading: boolean;
|
||||
externalAuthentications?: AvailableExternalAuthentications_shop_availableExternalAuthentications[];
|
||||
onExternalAuthentication: (pluginId: string) => void;
|
||||
onPasswordRecovery: () => void;
|
||||
onSubmit?(event: FormData);
|
||||
onSubmit?: (event: LoginFormData) => SubmitPromise;
|
||||
}
|
||||
|
||||
const LoginCard: React.FC<LoginCardProps> = props => {
|
||||
const { error, disableLoginButton, onPasswordRecovery, onSubmit } = props;
|
||||
const {
|
||||
error,
|
||||
externalError,
|
||||
disabled,
|
||||
loading,
|
||||
externalAuthentications = [],
|
||||
onExternalAuthentication,
|
||||
onPasswordRecovery,
|
||||
onSubmit
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
const intl = useIntl();
|
||||
|
||||
let initialFormData = { email: "", password: "" };
|
||||
if (DEMO_MODE) {
|
||||
initialFormData = {
|
||||
email: "admin@example.com",
|
||||
password: "admin"
|
||||
};
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={classes.loading}>
|
||||
<CircularProgress size={128} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form initial={initialFormData} onSubmit={onSubmit}>
|
||||
<LoginForm onSubmit={onSubmit}>
|
||||
{({ change: handleChange, data, submit: handleSubmit }) => (
|
||||
<>
|
||||
{error && (
|
||||
|
@ -73,6 +91,13 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{externalError && (
|
||||
<div className={classes.panel} data-test="loginErrorMessage">
|
||||
<Typography variant="caption">
|
||||
<FormattedMessage defaultMessage="Sorry, login went wrong. Please try again." />
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
|
@ -84,6 +109,7 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
inputProps={{
|
||||
"data-test": "email"
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<TextField
|
||||
|
@ -99,13 +125,14 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
inputProps={{
|
||||
"data-test": "password"
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<FormSpacer />
|
||||
<div className={classes.buttonContainer}>
|
||||
<Button
|
||||
className={classes.loginButton}
|
||||
color="primary"
|
||||
disabled={disableLoginButton}
|
||||
disabled={disabled}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
type="submit"
|
||||
|
@ -115,15 +142,56 @@ const LoginCard: React.FC<LoginCardProps> = props => {
|
|||
</Button>
|
||||
</div>
|
||||
<FormSpacer />
|
||||
<Typography className={classes.link} onClick={onPasswordRecovery}>
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
defaultMessage="Reset your password"
|
||||
description="button"
|
||||
defaultMessage="Forgot password? {resetPasswordLink}"
|
||||
description="description"
|
||||
values={{
|
||||
resetPasswordLink: (
|
||||
<a className={classes.link} onClick={onPasswordRecovery}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Use this link to recover it"
|
||||
description="link"
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
{externalAuthentications.length > 0 && (
|
||||
<>
|
||||
<FormSpacer />
|
||||
<Divider />
|
||||
<FormSpacer />
|
||||
<Typography>
|
||||
<FormattedMessage
|
||||
defaultMessage="or login using"
|
||||
description="description"
|
||||
/>
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{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";
|
||||
|
|
73
src/auth/components/LoginPage/form.tsx
Normal file
73
src/auth/components/LoginPage/form.tsx
Normal 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;
|
58
src/auth/hooks/useAuthProvider.ts
Normal file
58
src/auth/hooks/useAuthProvider.ts
Normal 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
|
||||
};
|
||||
}
|
262
src/auth/hooks/useExternalAuthProvider.ts
Normal file
262
src/auth/hooks/useExternalAuthProvider.ts
Normal 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
|
||||
};
|
||||
}
|
204
src/auth/hooks/useSaleorAuthProvider.ts
Normal file
204
src/auth/hooks/useSaleorAuthProvider.ts
Normal 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
|
||||
};
|
||||
}
|
|
@ -1,32 +1,57 @@
|
|||
import { User } from "@saleor/fragments/types/User";
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { parse as parseQs } from "qs";
|
||||
import React, { MutableRefObject } from "react";
|
||||
import { Route, RouteComponentProps, Switch } from "react-router-dom";
|
||||
|
||||
import Layout from "./components/Layout";
|
||||
import {
|
||||
ExternalLoginInput,
|
||||
RequestExternalLoginInput
|
||||
} from "./hooks/useExternalAuthProvider";
|
||||
import { ExternalObtainAccessTokens_externalObtainAccessTokens } from "./types/ExternalObtainAccessTokens";
|
||||
import { TokenAuth_tokenCreate } from "./types/TokenAuth";
|
||||
import {
|
||||
LoginUrlQueryParams,
|
||||
newPasswordPath,
|
||||
passwordResetPath,
|
||||
passwordResetSuccessPath
|
||||
} from "./urls";
|
||||
import LoginView from "./views/Login";
|
||||
import LoginViewComponent from "./views/Login";
|
||||
import NewPassword from "./views/NewPassword";
|
||||
import ResetPassword from "./views/ResetPassword";
|
||||
import ResetPasswordSuccess from "./views/ResetPasswordSuccess";
|
||||
|
||||
const LoginView: React.FC<RouteComponentProps<any>> = () => {
|
||||
const qs = parseQs(location.search.substr(1));
|
||||
const params: LoginUrlQueryParams = qs;
|
||||
|
||||
return <LoginViewComponent params={params} />;
|
||||
};
|
||||
|
||||
interface UserContext {
|
||||
login: (username: string, password: string) => void;
|
||||
login: (username: string, password: string) => Promise<TokenAuth_tokenCreate>;
|
||||
loginByExternalPlugin: (
|
||||
input: ExternalLoginInput
|
||||
) => Promise<ExternalObtainAccessTokens_externalObtainAccessTokens>;
|
||||
loginByToken: (auth: string, csrf: string, user: User) => void;
|
||||
logout: () => void;
|
||||
requestLoginByExternalPlugin: (
|
||||
pluginId: string,
|
||||
input: RequestExternalLoginInput
|
||||
) => Promise<void>;
|
||||
tokenAuthLoading: boolean;
|
||||
tokenRefresh: () => Promise<boolean>;
|
||||
tokenVerifyLoading: boolean;
|
||||
user?: User;
|
||||
autologinPromise?: MutableRefObject<Promise<any>>;
|
||||
}
|
||||
|
||||
export const UserContext = React.createContext<UserContext>({
|
||||
login: undefined,
|
||||
loginByExternalPlugin: undefined,
|
||||
loginByToken: undefined,
|
||||
logout: undefined,
|
||||
requestLoginByExternalPlugin: undefined,
|
||||
tokenAuthLoading: false,
|
||||
tokenRefresh: undefined,
|
||||
tokenVerifyLoading: false
|
||||
|
|
|
@ -82,3 +82,52 @@ export const SetPasswordMutation = TypedMutation<
|
|||
SetPassword,
|
||||
SetPasswordVariables
|
||||
>(setPassword);
|
||||
|
||||
export const externalAuthenticationUrlMutation = gql`
|
||||
${accountErrorFragment}
|
||||
mutation ExternalAuthenticationUrl($pluginId: String!, $input: JSONString!) {
|
||||
externalAuthenticationUrl(pluginId: $pluginId, input: $input) {
|
||||
authenticationData
|
||||
errors: accountErrors {
|
||||
...AccountErrorFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const externalObtainAccessTokensMutation = gql`
|
||||
${accountErrorFragment}
|
||||
${fragmentUser}
|
||||
mutation ExternalObtainAccessTokens($pluginId: String!, $input: JSONString!) {
|
||||
externalObtainAccessTokens(pluginId: $pluginId, input: $input) {
|
||||
token
|
||||
csrfToken
|
||||
user {
|
||||
...User
|
||||
}
|
||||
errors: accountErrors {
|
||||
...AccountErrorFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const externalTokenRefreshMutation = gql`
|
||||
mutation ExternalRefreshToken($pluginId: String!, $input: JSONString!) {
|
||||
externalRefresh(pluginId: $pluginId, input: $input) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const externalTokenVerifyMutation = gql`
|
||||
${fragmentUser}
|
||||
mutation ExternalVerifyToken($pluginId: String!, $input: JSONString!) {
|
||||
externalVerify(pluginId: $pluginId, input: $input) {
|
||||
verifyData
|
||||
user {
|
||||
...User
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
12
src/auth/queries.ts
Normal file
12
src/auth/queries.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import gql from "graphql-tag";
|
||||
|
||||
export const availableExternalAuthentications = gql`
|
||||
query AvailableExternalAuthentications {
|
||||
shop {
|
||||
availableExternalAuthentications {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
22
src/auth/types/AvailableExternalAuthentications.ts
Normal file
22
src/auth/types/AvailableExternalAuthentications.ts
Normal 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;
|
||||
}
|
30
src/auth/types/ExternalAuthenticationUrl.ts
Normal file
30
src/auth/types/ExternalAuthenticationUrl.ts
Normal 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;
|
||||
}
|
54
src/auth/types/ExternalObtainAccessTokens.ts
Normal file
54
src/auth/types/ExternalObtainAccessTokens.ts
Normal 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;
|
||||
}
|
21
src/auth/types/ExternalRefreshToken.ts
Normal file
21
src/auth/types/ExternalRefreshToken.ts
Normal 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;
|
||||
}
|
46
src/auth/types/ExternalVerifyToken.ts
Normal file
46
src/auth/types/ExternalVerifyToken.ts
Normal 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;
|
||||
}
|
|
@ -31,6 +31,7 @@ export interface SetPassword_setPassword_user {
|
|||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isStaff: boolean;
|
||||
userPermissions: (SetPassword_setPassword_user_userPermissions | null)[] | null;
|
||||
avatar: SetPassword_setPassword_user_avatar | null;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export interface TokenAuth_tokenCreate_user {
|
|||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isStaff: boolean;
|
||||
userPermissions: (TokenAuth_tokenCreate_user_userPermissions | null)[] | null;
|
||||
avatar: TokenAuth_tokenCreate_user_avatar | null;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export interface VerifyToken_tokenVerify_user {
|
|||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isStaff: boolean;
|
||||
userPermissions: (VerifyToken_tokenVerify_user_userPermissions | null)[] | null;
|
||||
avatar: VerifyToken_tokenVerify_user_avatar | null;
|
||||
}
|
||||
|
|
|
@ -7,9 +7,18 @@ export const passwordResetSuccessPath = "/reset-password/success/";
|
|||
export const passwordResetSuccessUrl = passwordResetSuccessPath;
|
||||
|
||||
export const newPasswordPath = "/new-password/";
|
||||
|
||||
export const loginCallbackPath = "/login/callback/";
|
||||
|
||||
export interface NewPasswordUrlQueryParams {
|
||||
email: string;
|
||||
token: string;
|
||||
}
|
||||
export const newPasswordUrl = (params?: NewPasswordUrlQueryParams) =>
|
||||
newPasswordPath + "?" + stringifyQs(params);
|
||||
|
||||
export interface LoginOpenidconnectUrlQueryParams {
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
export type LoginUrlQueryParams = LoginOpenidconnectUrlQueryParams;
|
||||
|
|
|
@ -1,20 +1,93 @@
|
|||
import { APP_DEFAULT_URI, APP_MOUNT_URI } from "@saleor/config";
|
||||
import useNavigator from "@saleor/hooks/useNavigator";
|
||||
import useUser from "@saleor/hooks/useUser";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useQuery } from "react-apollo";
|
||||
import urlJoin from "url-join";
|
||||
import useRouter from "use-react-router";
|
||||
|
||||
import LoginPage, { FormData } from "../components/LoginPage";
|
||||
import { passwordResetUrl } from "../urls";
|
||||
import LoginPage from "../components/LoginPage";
|
||||
import { LoginFormData } from "../components/LoginPage/form";
|
||||
import { availableExternalAuthentications } from "../queries";
|
||||
import { AvailableExternalAuthentications } from "../types/AvailableExternalAuthentications";
|
||||
import {
|
||||
loginCallbackPath,
|
||||
LoginUrlQueryParams,
|
||||
passwordResetUrl
|
||||
} from "../urls";
|
||||
|
||||
const LoginView: React.FC = () => {
|
||||
interface LoginViewProps {
|
||||
params: LoginUrlQueryParams;
|
||||
}
|
||||
|
||||
const LoginView: React.FC<LoginViewProps> = ({ params }) => {
|
||||
const navigate = useNavigator();
|
||||
const { login, user, tokenAuthLoading } = useUser();
|
||||
const { location } = useRouter();
|
||||
const {
|
||||
login,
|
||||
requestLoginByExternalPlugin,
|
||||
loginByExternalPlugin,
|
||||
tokenAuthLoading
|
||||
} = useUser();
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isExternalError, setIsExternalError] = useState(false);
|
||||
const {
|
||||
data: externalAuthentications,
|
||||
loading: externalAuthenticationsLoading
|
||||
} = useQuery<AvailableExternalAuthentications>(
|
||||
availableExternalAuthentications
|
||||
);
|
||||
|
||||
const handleSubmit = (data: FormData) => login(data.email, data.password);
|
||||
const handleSubmit = async (data: LoginFormData) => {
|
||||
const result = await login(data.email, data.password);
|
||||
const errors = result?.errors || [];
|
||||
|
||||
setIsExternalError(false);
|
||||
setIsError(!result || errors?.length > 0);
|
||||
return errors;
|
||||
};
|
||||
|
||||
const handleRequestExternalAuthentication = (pluginId: string) =>
|
||||
requestLoginByExternalPlugin(pluginId, {
|
||||
redirectUri: urlJoin(
|
||||
window.location.origin,
|
||||
APP_MOUNT_URI === APP_DEFAULT_URI ? "" : APP_MOUNT_URI,
|
||||
loginCallbackPath
|
||||
)
|
||||
});
|
||||
|
||||
const handleExternalAuthentication = async (code: string, state: string) => {
|
||||
const result = await loginByExternalPlugin({ code, state });
|
||||
const errors = result?.errors || [];
|
||||
|
||||
setIsError(false);
|
||||
if (!result || errors?.length > 0) {
|
||||
setIsExternalError(true);
|
||||
} else {
|
||||
navigate(APP_DEFAULT_URI);
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { code, state } = params;
|
||||
const isCallbackPath = location.pathname.includes(loginCallbackPath);
|
||||
|
||||
if (code && state && isCallbackPath) {
|
||||
handleExternalAuthentication(code, state);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoginPage
|
||||
error={user === null}
|
||||
disableLoginButton={tokenAuthLoading}
|
||||
error={isError}
|
||||
externalError={isExternalError}
|
||||
disabled={tokenAuthLoading}
|
||||
externalAuthentications={
|
||||
externalAuthentications?.shop?.availableExternalAuthentications
|
||||
}
|
||||
loading={externalAuthenticationsLoading || tokenAuthLoading}
|
||||
onExternalAuthentication={handleRequestExternalAuthentication}
|
||||
onPasswordRecovery={() => navigate(passwordResetUrl)}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
|
|
|
@ -33,7 +33,7 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const formErrors = getFormErrors(["name", "descriptionJson"], errors);
|
||||
const formErrors = getFormErrors(["name", "description"], errors);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
@ -59,8 +59,8 @@ export const CategoryDetailsForm: React.FC<CategoryDetailsFormProps> = ({
|
|||
<RichTextEditor
|
||||
data={data.description}
|
||||
disabled={disabled}
|
||||
error={!!formErrors.descriptionJson}
|
||||
helperText={getProductErrorMessage(formErrors.descriptionJson, intl)}
|
||||
error={!!formErrors.description}
|
||||
helperText={getProductErrorMessage(formErrors.description, intl)}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Category Description"
|
||||
})}
|
||||
|
|
|
@ -56,7 +56,7 @@ function useCategoryUpdateForm(
|
|||
slug: category?.slug || ""
|
||||
});
|
||||
const [description, changeDescription] = useRichText({
|
||||
initial: category?.descriptionJson,
|
||||
initial: category?.description,
|
||||
triggerChange
|
||||
});
|
||||
|
||||
|
|
|
@ -105,7 +105,7 @@ export const category: (
|
|||
startCursor: "YXJyYXljb25uZWN0aW9uOjA="
|
||||
}
|
||||
},
|
||||
descriptionJson: JSON.stringify(content),
|
||||
description: JSON.stringify(content),
|
||||
id: "Q2F0ZWdvcnk6NA==",
|
||||
metadata: [
|
||||
{
|
||||
|
|
|
@ -39,7 +39,7 @@ export interface CategoryCreate_categoryCreate_category {
|
|||
backgroundImage: CategoryCreate_categoryCreate_category_backgroundImage | null;
|
||||
name: string;
|
||||
slug: string;
|
||||
descriptionJson: any;
|
||||
description: any;
|
||||
seoDescription: string | null;
|
||||
seoTitle: string | null;
|
||||
parent: CategoryCreate_categoryCreate_category_parent | null;
|
||||
|
|
|
@ -165,7 +165,7 @@ export interface CategoryDetails_category {
|
|||
backgroundImage: CategoryDetails_category_backgroundImage | null;
|
||||
name: string;
|
||||
slug: string;
|
||||
descriptionJson: any;
|
||||
description: any;
|
||||
seoDescription: string | null;
|
||||
seoTitle: string | null;
|
||||
parent: CategoryDetails_category_parent | null;
|
||||
|
|
|
@ -39,7 +39,7 @@ export interface CategoryUpdate_categoryUpdate_category {
|
|||
backgroundImage: CategoryUpdate_categoryUpdate_category_backgroundImage | null;
|
||||
name: string;
|
||||
slug: string;
|
||||
descriptionJson: any;
|
||||
description: any;
|
||||
seoDescription: string | null;
|
||||
seoTitle: string | null;
|
||||
parent: CategoryUpdate_categoryUpdate_category_parent | null;
|
||||
|
|
|
@ -48,7 +48,7 @@ export const CategoryCreateView: React.FC<CategoryCreateViewProps> = ({
|
|||
const result = await createCategory({
|
||||
variables: {
|
||||
input: {
|
||||
descriptionJson: JSON.stringify(formData.description),
|
||||
description: JSON.stringify(formData.description),
|
||||
name: formData.name,
|
||||
seo: {
|
||||
description: formData.seoDescription,
|
||||
|
|
|
@ -188,7 +188,7 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
|
|||
id,
|
||||
input: {
|
||||
backgroundImageAlt: formData.backgroundImageAlt,
|
||||
descriptionJson: JSON.stringify(formData.description),
|
||||
description: JSON.stringify(formData.description),
|
||||
name: formData.name,
|
||||
seo: {
|
||||
description: formData.seoDescription,
|
||||
|
|
|
@ -20,6 +20,7 @@ import React from "react";
|
|||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import { useStyles } from "../styles";
|
||||
import { ExtendedFormHelperTextProps } from "./types";
|
||||
|
||||
export interface FormData {
|
||||
name: string;
|
||||
|
@ -80,6 +81,11 @@ export const ChannelForm: React.FC<ChannelFormProps> = ({
|
|||
helperText={getChannelsErrorMessage(formErrors?.slug, intl)}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
FormHelperTextProps={
|
||||
{
|
||||
"data-testid": "slug-text-input-helper-text"
|
||||
} as ExtendedFormHelperTextProps
|
||||
}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: "Slug",
|
||||
description: "channel slug"
|
||||
|
@ -124,8 +130,14 @@ export const ChannelForm: React.FC<ChannelFormProps> = ({
|
|||
<CardContent>
|
||||
{!!currencyCodes ? (
|
||||
<SingleAutocompleteSelectField
|
||||
data-test-id="channel-currency-select-input"
|
||||
allowCustomValues
|
||||
error={!!formErrors.currencyCode}
|
||||
FormHelperTextProps={
|
||||
{
|
||||
"data-testid": "currency-text-input-helper-text"
|
||||
} as ExtendedFormHelperTextProps
|
||||
}
|
||||
helperText={getChannelsErrorMessage(
|
||||
formErrors?.currencyCode,
|
||||
intl
|
||||
|
|
5
src/channels/components/ChannelForm/types.ts
Normal file
5
src/channels/components/ChannelForm/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { FormHelperTextProps } from "@material-ui/core/FormHelperText";
|
||||
|
||||
export type ExtendedFormHelperTextProps = FormHelperTextProps & {
|
||||
"data-testid": string;
|
||||
};
|
|
@ -1,10 +1,23 @@
|
|||
import { channelDetailsFragment } from "@saleor/fragments/channels";
|
||||
import {
|
||||
channelDetailsFragment,
|
||||
channelFragment
|
||||
} from "@saleor/fragments/channels";
|
||||
import makeQuery from "@saleor/hooks/makeQuery";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
import { BaseChannels } from "./types/BaseChannels";
|
||||
import { Channel, ChannelVariables } from "./types/Channel";
|
||||
import { Channels } from "./types/Channels";
|
||||
|
||||
export const channelsListBase = gql`
|
||||
${channelFragment}
|
||||
query BaseChannels {
|
||||
channels {
|
||||
...ChannelFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const channelsList = gql`
|
||||
${channelDetailsFragment}
|
||||
query Channels {
|
||||
|
@ -23,6 +36,9 @@ export const channelDetails = gql`
|
|||
}
|
||||
`;
|
||||
|
||||
export const useBaseChannelsList = makeQuery<BaseChannels, {}>(
|
||||
channelsListBase
|
||||
);
|
||||
export const useChannelsList = makeQuery<Channels, {}>(channelsList);
|
||||
export const useChannelDetails = makeQuery<Channel, ChannelVariables>(
|
||||
channelDetails
|
||||
|
|
20
src/channels/types/BaseChannels.ts
Normal file
20
src/channels/types/BaseChannels.ts
Normal 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;
|
||||
}
|
|
@ -33,7 +33,7 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const formErrors = getFormErrors(["name", "descriptionJson"], errors);
|
||||
const formErrors = getFormErrors(["name", "description"], errors);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
@ -57,8 +57,8 @@ const CollectionDetails: React.FC<CollectionDetailsProps> = ({
|
|||
<FormSpacer />
|
||||
<RichTextEditor
|
||||
data={data.description}
|
||||
error={!!formErrors.descriptionJson}
|
||||
helperText={getProductErrorMessage(formErrors.descriptionJson, intl)}
|
||||
error={!!formErrors.description}
|
||||
helperText={getProductErrorMessage(formErrors.description, intl)}
|
||||
label={intl.formatMessage(commonMessages.description)}
|
||||
name="description"
|
||||
disabled={disabled}
|
||||
|
|
|
@ -68,7 +68,7 @@ function useCollectionUpdateForm(
|
|||
slug: collection?.slug || ""
|
||||
});
|
||||
const [description, changeDescription] = useRichText({
|
||||
initial: collection?.descriptionJson,
|
||||
initial: collection?.description,
|
||||
triggerChange
|
||||
});
|
||||
|
||||
|
|
|
@ -157,7 +157,7 @@ export const collection: (
|
|||
publicationDate: null
|
||||
}
|
||||
],
|
||||
descriptionJson: JSON.stringify(content),
|
||||
description: JSON.stringify(content),
|
||||
id: "Q29sbGVjdGlvbjox",
|
||||
metadata: [
|
||||
{
|
||||
|
|
|
@ -102,7 +102,7 @@ export interface CollectionDetails_collection {
|
|||
privateMetadata: (CollectionDetails_collection_privateMetadata | null)[];
|
||||
backgroundImage: CollectionDetails_collection_backgroundImage | null;
|
||||
slug: string;
|
||||
descriptionJson: any;
|
||||
description: any;
|
||||
seoDescription: string | null;
|
||||
seoTitle: string | null;
|
||||
products: CollectionDetails_collection_products | null;
|
||||
|
|
|
@ -48,7 +48,7 @@ export interface CollectionUpdate_collectionUpdate_collection {
|
|||
privateMetadata: (CollectionUpdate_collectionUpdate_collection_privateMetadata | null)[];
|
||||
backgroundImage: CollectionUpdate_collectionUpdate_collection_backgroundImage | null;
|
||||
slug: string;
|
||||
descriptionJson: any;
|
||||
description: any;
|
||||
seoDescription: string | null;
|
||||
seoTitle: string | null;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export interface CreateCollection_collectionCreate_collection {
|
|||
privateMetadata: (CreateCollection_collectionCreate_collection_privateMetadata | null)[];
|
||||
backgroundImage: CreateCollection_collectionCreate_collection_backgroundImage | null;
|
||||
slug: string;
|
||||
descriptionJson: any;
|
||||
description: any;
|
||||
seoDescription: string | null;
|
||||
seoTitle: string | null;
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ export const CollectionCreate: React.FC<CollectionCreateProps> = ({
|
|||
input: {
|
||||
backgroundImage: formData.backgroundImage.value,
|
||||
backgroundImageAlt: formData.backgroundImageAlt,
|
||||
descriptionJson: JSON.stringify(formData.description),
|
||||
description: JSON.stringify(formData.description),
|
||||
name: formData.name,
|
||||
seo: {
|
||||
description: formData.seoDescription,
|
||||
|
|
|
@ -198,7 +198,7 @@ export const CollectionDetails: React.FC<CollectionDetailsProps> = ({
|
|||
const handleUpdate = async (formData: CollectionUpdateData) => {
|
||||
const input: CollectionInput = {
|
||||
backgroundImageAlt: formData.backgroundImageAlt,
|
||||
descriptionJson: JSON.stringify(formData.description),
|
||||
description: JSON.stringify(formData.description),
|
||||
name: formData.name,
|
||||
seo: {
|
||||
description: formData.seoDescription,
|
||||
|
|
|
@ -61,7 +61,11 @@ const AppHeader: React.FC<AppHeaderProps> = props => {
|
|||
{anchor =>
|
||||
anchor ? (
|
||||
<Portal container={anchor.current}>
|
||||
<div className={classes.root} onClick={onBack}>
|
||||
<div
|
||||
className={classes.root}
|
||||
onClick={onBack}
|
||||
data-test-id="app-header-back-button"
|
||||
>
|
||||
<ArrowBackIcon className={classes.backArrow} />
|
||||
{children ? (
|
||||
<Typography className={classes.title}>{children}</Typography>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { useAuth } from "@saleor/auth/AuthProvider";
|
||||
import { useChannelsList } from "@saleor/channels/queries";
|
||||
import { ChannelDetailsFragment } from "@saleor/fragments/types/ChannelDetailsFragment";
|
||||
import { useBaseChannelsList } from "@saleor/channels/queries";
|
||||
import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment";
|
||||
import useLocalStorage from "@saleor/hooks/useLocalStorage";
|
||||
import React from "react";
|
||||
|
||||
interface UseAppChannel {
|
||||
availableChannels: ChannelDetailsFragment[];
|
||||
channel: ChannelDetailsFragment;
|
||||
availableChannels: ChannelFragment[];
|
||||
channel: ChannelFragment;
|
||||
isPickerActive: boolean;
|
||||
refreshChannels: () => void;
|
||||
setChannel: (id: string) => void;
|
||||
|
@ -27,14 +27,14 @@ const AppChannelContext = React.createContext<AppChannelContextData>({
|
|||
export const AppChannelProvider: React.FC = ({ children }) => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const [selectedChannel, setSelectedChannel] = useLocalStorage("channel", "");
|
||||
const { data: channelData, refetch } = useChannelsList({
|
||||
const { data: channelData, refetch } = useBaseChannelsList({
|
||||
skip: !isAuthenticated
|
||||
});
|
||||
|
||||
const [isPickerActive, setPickerActive] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
if (!selectedChannel) {
|
||||
setSelectedChannel(channelData?.channels[0].id);
|
||||
if (!selectedChannel && channelData?.channels) {
|
||||
setSelectedChannel(channelData.channels[0].id);
|
||||
}
|
||||
}, [channelData]);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
import { ChannelDetailsFragment } from "@saleor/fragments/types/ChannelDetailsFragment";
|
||||
import { ChannelFragment } from "@saleor/fragments/types/ChannelFragment";
|
||||
import { ChannelProps } from "@saleor/types";
|
||||
import { mapNodeToChoice } from "@saleor/utils/maps";
|
||||
import React from "react";
|
||||
|
@ -22,7 +22,7 @@ const useStyles = makeStyles(
|
|||
);
|
||||
|
||||
export interface AppChannelSelectProps extends ChannelProps {
|
||||
channels: ChannelDetailsFragment[];
|
||||
channels: ChannelFragment[];
|
||||
disabled: boolean;
|
||||
onChannelSelect: (id: string) => void;
|
||||
}
|
||||
|
|
|
@ -239,8 +239,11 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
|
|||
</div>
|
||||
<main className={classes.view}>
|
||||
{appState.error
|
||||
? appState.error === "unhandled" && (
|
||||
<ErrorPage onBack={handleErrorBack} />
|
||||
? appState.error.type === "unhandled" && (
|
||||
<ErrorPage
|
||||
id={appState.error.id}
|
||||
onBack={handleErrorBack}
|
||||
/>
|
||||
)
|
||||
: children}
|
||||
</main>
|
||||
|
|
|
@ -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;
|
2
src/components/AssignAttributeValueDialog/index.ts
Normal file
2
src/components/AssignAttributeValueDialog/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./AssignAttributeValueDialog";
|
||||
export * from "./AssignAttributeValueDialog";
|
|
@ -12,7 +12,10 @@ const props: AttributesProps = {
|
|||
loading: false,
|
||||
onChange: () => undefined,
|
||||
onFileChange: () => undefined,
|
||||
onMultiChange: () => undefined
|
||||
onMultiChange: () => undefined,
|
||||
onReferencesAddClick: () => undefined,
|
||||
onReferencesRemove: () => undefined,
|
||||
onReferencesReorder: () => undefined
|
||||
};
|
||||
|
||||
storiesOf("Attributes / Attributes", module)
|
||||
|
|
|
@ -4,8 +4,8 @@ import IconButton from "@material-ui/core/IconButton";
|
|||
import makeStyles from "@material-ui/core/styles/makeStyles";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
|
||||
import { AttributeReference } from "@saleor/attributes/utils/data";
|
||||
import CardTitle from "@saleor/components/CardTitle";
|
||||
import Grid from "@saleor/components/Grid";
|
||||
import Hr from "@saleor/components/Hr";
|
||||
import MultiAutocompleteSelectField, {
|
||||
MultiAutocompleteChoiceType
|
||||
|
@ -17,7 +17,11 @@ import { AttributeValueFragment } from "@saleor/fragments/types/AttributeValueFr
|
|||
import { PageErrorWithAttributesFragment } from "@saleor/fragments/types/PageErrorWithAttributesFragment";
|
||||
import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment";
|
||||
import { FormsetAtomicData, FormsetChange } from "@saleor/hooks/useFormset";
|
||||
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
|
||||
import { ReorderEvent } from "@saleor/types";
|
||||
import {
|
||||
AttributeEntityTypeEnum,
|
||||
AttributeInputTypeEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
import { getProductErrorMessage } from "@saleor/utils/errors";
|
||||
import getPageErrorMessage from "@saleor/utils/errors/page";
|
||||
import classNames from "classnames";
|
||||
|
@ -30,14 +34,21 @@ import {
|
|||
} from "react-intl";
|
||||
|
||||
import FileUploadField, { FileChoiceType } from "../FileUploadField";
|
||||
import SortableChipsField, {
|
||||
SortableChipsFieldValueType
|
||||
} from "../SortableChipsField";
|
||||
import BasicAttributeRow from "./BasicAttributeRow";
|
||||
import ExtendedAttributeRow from "./ExtendedAttributeRow";
|
||||
import { VariantAttributeScope } from "./types";
|
||||
|
||||
export interface AttributeInputData {
|
||||
inputType: AttributeInputTypeEnum;
|
||||
entityType?: AttributeEntityTypeEnum;
|
||||
variantAttributeScope?: VariantAttributeScope;
|
||||
isRequired: boolean;
|
||||
values: AttributeValueFragment[];
|
||||
selectedValues?: AttributeValueFragment[];
|
||||
references?: AttributeReference[];
|
||||
}
|
||||
export type AttributeInput = FormsetAtomicData<AttributeInputData, string[]>;
|
||||
export type AttributeFileInput = FormsetAtomicData<AttributeInputData, File[]>;
|
||||
|
@ -49,9 +60,12 @@ export interface AttributesProps {
|
|||
ProductErrorWithAttributesFragment | PageErrorWithAttributesFragment
|
||||
>;
|
||||
title?: React.ReactNode;
|
||||
onChange: FormsetChange;
|
||||
onMultiChange: FormsetChange;
|
||||
onFileChange?: FormsetChange; // TODO: temporairy optional, should be changed to required, after all pages implement it
|
||||
onChange: FormsetChange<string>;
|
||||
onMultiChange: FormsetChange<string>;
|
||||
onFileChange: FormsetChange<File>;
|
||||
onReferencesRemove: FormsetChange<string[]>;
|
||||
onReferencesAddClick: (attribute: AttributeInput) => void;
|
||||
onReferencesReorder: FormsetChange<ReorderEvent>;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(
|
||||
|
@ -123,6 +137,10 @@ function getMultiChoices(
|
|||
function getMultiDisplayValue(
|
||||
attribute: AttributeInput
|
||||
): MultiAutocompleteChoiceType[] {
|
||||
if (!attribute.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return attribute.value.map(attributeValue => {
|
||||
const definedAttributeValue = attribute.data.values.find(
|
||||
definedValue => definedValue.slug === attributeValue
|
||||
|
@ -141,6 +159,40 @@ function getMultiDisplayValue(
|
|||
});
|
||||
}
|
||||
|
||||
function getReferenceDisplayValue(
|
||||
attribute: AttributeInput
|
||||
): SortableChipsFieldValueType[] {
|
||||
if (!attribute.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return attribute.value.map(attributeValue => {
|
||||
const definedAttributeValue = attribute.data.values.find(
|
||||
definedValue => definedValue.reference === attributeValue
|
||||
);
|
||||
// If value has been previously assigned, use it's data
|
||||
if (!!definedAttributeValue) {
|
||||
return {
|
||||
label: definedAttributeValue.name,
|
||||
value: definedAttributeValue.reference
|
||||
};
|
||||
}
|
||||
|
||||
const definedAttributeReference = attribute.data.references?.find(
|
||||
reference => reference.value === attributeValue
|
||||
);
|
||||
// If value has not been yet assigned, use data of reference
|
||||
if (!!definedAttributeReference) {
|
||||
return definedAttributeReference;
|
||||
}
|
||||
|
||||
return {
|
||||
label: attributeValue,
|
||||
value: attributeValue
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getSingleChoices(
|
||||
values: AttributeValueFragment[]
|
||||
): SingleAutocompleteChoiceType[] {
|
||||
|
@ -151,7 +203,7 @@ function getSingleChoices(
|
|||
}
|
||||
|
||||
function getFileChoice(attribute: AttributeInput): FileChoiceType {
|
||||
const attributeValue = attribute.value[0];
|
||||
const attributeValue = attribute.value?.length > 0 && attribute.value[0];
|
||||
|
||||
const definedAttributeValue = attribute.data.values.find(
|
||||
definedValue => definedValue.slug === attributeValue
|
||||
|
@ -210,7 +262,10 @@ const Attributes: React.FC<AttributesProps> = ({
|
|||
title,
|
||||
onChange,
|
||||
onMultiChange,
|
||||
onFileChange
|
||||
onFileChange,
|
||||
onReferencesRemove,
|
||||
onReferencesAddClick,
|
||||
onReferencesReorder
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const classes = useStyles({});
|
||||
|
@ -255,76 +310,95 @@ const Attributes: React.FC<AttributesProps> = ({
|
|||
return (
|
||||
<React.Fragment key={attribute.id}>
|
||||
{attributeIndex > 0 && <Hr />}
|
||||
<Grid className={classes.attributeSection} variant="uniform">
|
||||
<div
|
||||
className={classes.attributeSectionLabel}
|
||||
data-test="attribute-label"
|
||||
{attribute.data.inputType ===
|
||||
AttributeInputTypeEnum.REFERENCE ? (
|
||||
<ExtendedAttributeRow
|
||||
label={attribute.label}
|
||||
selectLabel={intl.formatMessage({
|
||||
defaultMessage: "Assign references",
|
||||
description: "button label"
|
||||
})}
|
||||
onSelect={() => onReferencesAddClick(attribute)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Typography>{attribute.label}</Typography>
|
||||
</div>
|
||||
<div data-test="attribute-value">
|
||||
{attribute.data.inputType ===
|
||||
AttributeInputTypeEnum.FILE ? (
|
||||
<FileUploadField
|
||||
className={classes.fileField}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
file={getFileChoice(attribute)}
|
||||
onFileUpload={file =>
|
||||
onFileChange(attribute.id, file)
|
||||
}
|
||||
onFileDelete={() =>
|
||||
onFileChange(attribute.id, undefined)
|
||||
}
|
||||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
inputProps={{
|
||||
name: `attribute:${attribute.label}`
|
||||
}}
|
||||
/>
|
||||
) : attribute.data.inputType ===
|
||||
AttributeInputTypeEnum.DROPDOWN ? (
|
||||
<SingleAutocompleteSelectField
|
||||
choices={getSingleChoices(attribute.data.values)}
|
||||
disabled={disabled}
|
||||
displayValue={
|
||||
attribute.data.values.find(
|
||||
value => value.slug === attribute.value[0]
|
||||
)?.name ||
|
||||
attribute.value[0] ||
|
||||
""
|
||||
}
|
||||
emptyOption={!attribute.data.isRequired}
|
||||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
label={intl.formatMessage(messages.valueLabel)}
|
||||
value={attribute.value[0]}
|
||||
onChange={event =>
|
||||
onChange(attribute.id, event.target.value)
|
||||
}
|
||||
allowCustomValues={!attribute.data.isRequired}
|
||||
/>
|
||||
) : (
|
||||
<MultiAutocompleteSelectField
|
||||
choices={getMultiChoices(attribute.data.values)}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Grid>
|
||||
<SortableChipsField
|
||||
values={getReferenceDisplayValue(attribute)}
|
||||
onValueDelete={value =>
|
||||
onReferencesRemove(
|
||||
attribute.id,
|
||||
attribute.value?.filter(id => id !== value)
|
||||
)
|
||||
}
|
||||
onValueReorder={event =>
|
||||
onReferencesReorder(attribute.id, event)
|
||||
}
|
||||
loading={loading}
|
||||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
/>
|
||||
</ExtendedAttributeRow>
|
||||
) : attribute.data.inputType ===
|
||||
AttributeInputTypeEnum.FILE ? (
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
<FileUploadField
|
||||
className={classes.fileField}
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
file={getFileChoice(attribute)}
|
||||
onFileUpload={file => onFileChange(attribute.id, file)}
|
||||
onFileDelete={() =>
|
||||
onFileChange(attribute.id, undefined)
|
||||
}
|
||||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
inputProps={{
|
||||
name: `attribute:${attribute.label}`
|
||||
}}
|
||||
/>
|
||||
</BasicAttributeRow>
|
||||
) : attribute.data.inputType ===
|
||||
AttributeInputTypeEnum.DROPDOWN ? (
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
<SingleAutocompleteSelectField
|
||||
choices={getSingleChoices(attribute.data.values)}
|
||||
disabled={disabled}
|
||||
displayValue={
|
||||
attribute.data.values.find(
|
||||
value => value.slug === attribute.value[0]
|
||||
)?.name ||
|
||||
attribute.value[0] ||
|
||||
""
|
||||
}
|
||||
emptyOption={!attribute.data.isRequired}
|
||||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
label={intl.formatMessage(messages.valueLabel)}
|
||||
value={attribute.value[0]}
|
||||
onChange={event =>
|
||||
onChange(attribute.id, event.target.value)
|
||||
}
|
||||
allowCustomValues={!attribute.data.isRequired}
|
||||
/>
|
||||
</BasicAttributeRow>
|
||||
) : (
|
||||
<BasicAttributeRow label={attribute.label}>
|
||||
<MultiAutocompleteSelectField
|
||||
choices={getMultiChoices(attribute.data.values)}
|
||||
displayValues={getMultiDisplayValue(attribute)}
|
||||
disabled={disabled}
|
||||
error={!!error}
|
||||
helperText={getErrorMessage(error, intl)}
|
||||
label={intl.formatMessage(messages.multipleValueLable)}
|
||||
name={`attribute:${attribute.label}`}
|
||||
value={attribute.value}
|
||||
onChange={event =>
|
||||
onMultiChange(attribute.id, event.target.value)
|
||||
}
|
||||
allowCustomValues={!attribute.data.isRequired}
|
||||
/>
|
||||
</BasicAttributeRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
|
44
src/components/Attributes/BasicAttributeRow.tsx
Normal file
44
src/components/Attributes/BasicAttributeRow.tsx
Normal 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;
|
65
src/components/Attributes/ExtendedAttributeRow.tsx
Normal file
65
src/components/Attributes/ExtendedAttributeRow.tsx
Normal 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;
|
|
@ -1,4 +1,7 @@
|
|||
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
|
||||
import {
|
||||
AttributeEntityTypeEnum,
|
||||
AttributeInputTypeEnum
|
||||
} from "@saleor/types/globalTypes";
|
||||
|
||||
import { AttributeInput } from "./Attributes";
|
||||
|
||||
|
@ -12,6 +15,7 @@ const DROPDOWN_ATTRIBUTE: AttributeInput = {
|
|||
file: null,
|
||||
id: "fdinugiffgffd",
|
||||
name: "Dropdown First Value",
|
||||
reference: null,
|
||||
slug: "dropdown-first-value"
|
||||
},
|
||||
{
|
||||
|
@ -19,6 +23,7 @@ const DROPDOWN_ATTRIBUTE: AttributeInput = {
|
|||
file: null,
|
||||
id: "fdhfdhdihidff",
|
||||
name: "Dropdown Second Value",
|
||||
reference: null,
|
||||
slug: "dropdown-second-value"
|
||||
}
|
||||
]
|
||||
|
@ -38,6 +43,7 @@ const MULTISELECT_ATTRIBUTE: AttributeInput = {
|
|||
file: null,
|
||||
id: "terteretregtt",
|
||||
name: "Multiselect First Value",
|
||||
reference: null,
|
||||
slug: "multiselect-first-value"
|
||||
},
|
||||
{
|
||||
|
@ -45,6 +51,7 @@ const MULTISELECT_ATTRIBUTE: AttributeInput = {
|
|||
file: null,
|
||||
id: "tyueyryetopwr",
|
||||
name: "Multiselect Second Value",
|
||||
reference: null,
|
||||
slug: "multiselect-second-value"
|
||||
},
|
||||
{
|
||||
|
@ -52,6 +59,7 @@ const MULTISELECT_ATTRIBUTE: AttributeInput = {
|
|||
file: null,
|
||||
id: "truiwrtweirqd",
|
||||
name: "Multiselect Third Value",
|
||||
reference: null,
|
||||
slug: "multiselect-third-value"
|
||||
}
|
||||
]
|
||||
|
@ -75,19 +83,72 @@ const FILE_ATTRIBUTE: AttributeInput = {
|
|||
},
|
||||
id: "gdghdgdhkkdae",
|
||||
name: "File First Value",
|
||||
reference: null,
|
||||
slug: "file-first-value"
|
||||
}
|
||||
]
|
||||
},
|
||||
id: "ifudbgidfsb",
|
||||
id: "fguygygugyu",
|
||||
label: "File Attribute",
|
||||
value: []
|
||||
};
|
||||
|
||||
const REFERENCE_ATTRIBUTE: AttributeInput = {
|
||||
data: {
|
||||
entityType: AttributeEntityTypeEnum.PAGE,
|
||||
inputType: AttributeInputTypeEnum.REFERENCE,
|
||||
isRequired: true,
|
||||
references: [
|
||||
{
|
||||
label: "References First Value",
|
||||
value: "vbnhgcvjhbvhj"
|
||||
},
|
||||
{
|
||||
label: "References Second Value",
|
||||
value: "gucngdfdfvdvd"
|
||||
},
|
||||
{
|
||||
label: "References Third Value",
|
||||
value: "dfdfdsfdsfdse"
|
||||
}
|
||||
],
|
||||
values: [
|
||||
{
|
||||
__typename: "AttributeValue",
|
||||
file: null,
|
||||
id: "vbnhgcvjhbvhj",
|
||||
name: "References First Value",
|
||||
reference: null,
|
||||
slug: "references-first-value"
|
||||
},
|
||||
{
|
||||
__typename: "AttributeValue",
|
||||
file: null,
|
||||
id: "gucngdfdfvdvd",
|
||||
name: "References Second Value",
|
||||
reference: null,
|
||||
slug: "references-second-value"
|
||||
},
|
||||
{
|
||||
__typename: "AttributeValue",
|
||||
file: null,
|
||||
id: "dfdfdsfdsfdse",
|
||||
name: "References Third Value",
|
||||
reference: null,
|
||||
slug: "references-third-value"
|
||||
}
|
||||
]
|
||||
},
|
||||
id: "kclsmcdsmcs",
|
||||
label: "References Attribute",
|
||||
value: []
|
||||
};
|
||||
|
||||
export const ATTRIBUTES: AttributeInput[] = [
|
||||
DROPDOWN_ATTRIBUTE,
|
||||
MULTISELECT_ATTRIBUTE,
|
||||
FILE_ATTRIBUTE
|
||||
FILE_ATTRIBUTE,
|
||||
REFERENCE_ATTRIBUTE
|
||||
];
|
||||
|
||||
export const ATTRIBUTES_SELECTED: AttributeInput[] = [
|
||||
|
@ -105,5 +166,13 @@ export const ATTRIBUTES_SELECTED: AttributeInput[] = [
|
|||
{
|
||||
...FILE_ATTRIBUTE,
|
||||
value: [FILE_ATTRIBUTE.data.values[0].slug]
|
||||
},
|
||||
{
|
||||
...REFERENCE_ATTRIBUTE,
|
||||
value: [
|
||||
REFERENCE_ATTRIBUTE.data.values[0].id,
|
||||
REFERENCE_ATTRIBUTE.data.values[1].id,
|
||||
REFERENCE_ATTRIBUTE.data.values[2].id
|
||||
]
|
||||
}
|
||||
];
|
||||
|
|
|
@ -17,6 +17,7 @@ const user: User = {
|
|||
email: "email@example.com",
|
||||
firstName: "User",
|
||||
id: "123",
|
||||
isStaff: true,
|
||||
lastName: "User",
|
||||
userPermissions: [
|
||||
{
|
||||
|
|
|
@ -417,7 +417,11 @@ export const ChannelsAvailability: React.FC<ChannelsAvailabilityProps> = props =
|
|||
userPermissions={user?.userPermissions || []}
|
||||
requiredPermissions={[PermissionEnum.MANAGE_CHANNELS]}
|
||||
>
|
||||
<Button color="primary" onClick={openModal}>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={openModal}
|
||||
data-test-id="channels-availiability-manage-button"
|
||||
>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Manage",
|
||||
description: "section header button"
|
||||
|
|
|
@ -77,7 +77,10 @@ export const ChannelsAvailabilityContent: React.FC<ChannelsAvailabilityContentPr
|
|||
<Typography className={classes.contentTitle}>
|
||||
<FormattedMessage defaultMessage="Channels A to Z" />
|
||||
</Typography>
|
||||
<div className={classes.scrollArea}>
|
||||
<div
|
||||
className={classes.scrollArea}
|
||||
data-test-id="manage-products-channels-availiability-list"
|
||||
>
|
||||
{filteredChannels?.length ? (
|
||||
filteredChannels.map(option => (
|
||||
<div key={option.id} className={classes.option}>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import Chip, { ChipProps } from "@saleor/components/Chip";
|
||||
import CardDecorator from "@saleor/storybook/CardDecorator";
|
||||
import Decorator from "@saleor/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import CardDecorator from "../../CardDecorator";
|
||||
import Decorator from "../../Decorator";
|
||||
|
||||
const props: ChipProps = {
|
||||
label: "Lorem Ipsum"
|
||||
};
|
|
@ -7,6 +7,7 @@ import SVG from "react-inlinesvg";
|
|||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export interface ErrorPageProps {
|
||||
id?: string | null;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
|
@ -31,6 +32,9 @@ const useStyles = makeStyles(
|
|||
margin: "0 auto",
|
||||
width: 830
|
||||
},
|
||||
errorId: {
|
||||
marginTop: theme.spacing(3)
|
||||
},
|
||||
innerContainer: {
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
order: 1,
|
||||
|
@ -58,7 +62,7 @@ const useStyles = makeStyles(
|
|||
);
|
||||
|
||||
const ErrorPage: React.FC<ErrorPageProps> = props => {
|
||||
const { onBack } = props;
|
||||
const { onBack, id } = props;
|
||||
|
||||
const classes = useStyles(props);
|
||||
|
||||
|
@ -79,6 +83,11 @@ const ErrorPage: React.FC<ErrorPageProps> = props => {
|
|||
<Typography>
|
||||
<FormattedMessage defaultMessage="Don't worry, everything is gonna be fine" />
|
||||
</Typography>
|
||||
{!!id && (
|
||||
<Typography variant="subtitle2" className={classes.errorId}>
|
||||
Error ID: {id}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
|
|
|
@ -3,7 +3,6 @@ import FormControl from "@material-ui/core/FormControl";
|
|||
import FormHelperText from "@material-ui/core/FormHelperText";
|
||||
import InputLabel from "@material-ui/core/InputLabel";
|
||||
import classNames from "classnames";
|
||||
import Undo from "editorjs-undo";
|
||||
import React from "react";
|
||||
|
||||
import { RichTextEditorContentProps, tools } from "./RichTextEditorContent";
|
||||
|
@ -34,6 +33,8 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|||
const [isFocused, setFocus] = React.useState(false);
|
||||
const editor = React.useRef<EditorJS>();
|
||||
const editorContainer = React.useRef<HTMLDivElement>();
|
||||
const prevTogglePromise = React.useRef<Promise<boolean>>(); // used to await subsequent toggle invocations
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (data) {
|
||||
|
@ -46,8 +47,10 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|||
onChange(savedData);
|
||||
},
|
||||
onReady: () => {
|
||||
const undo = new Undo({ editor });
|
||||
undo.initialize(data);
|
||||
// FIXME: This throws an error and is not working
|
||||
// const undo = new Undo({ editor });
|
||||
// undo.initialize(data);
|
||||
|
||||
if (onReady) {
|
||||
onReady();
|
||||
}
|
||||
|
@ -62,10 +65,20 @@ const RichTextEditor: React.FC<RichTextEditorProps> = ({
|
|||
// Rerender editor only if changed from undefined to defined state
|
||||
[data === undefined]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (editor.current?.readOnly) {
|
||||
editor.current.readOnly.toggle(disabled);
|
||||
}
|
||||
const toggle = async () => {
|
||||
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]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { InputProps } from "@material-ui/core/Input";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import { ExtendedFormHelperTextProps } from "@saleor/channels/components/ChannelForm/types";
|
||||
import { FetchMoreProps } from "@saleor/types";
|
||||
import classNames from "classnames";
|
||||
import Downshift, { ControllerStateAndHelpers } from "downshift";
|
||||
|
@ -42,6 +43,7 @@ export interface SingleAutocompleteSelectFieldProps
|
|||
InputProps?: InputProps;
|
||||
fetchChoices?: (value: string) => void;
|
||||
onChange: (event: React.ChangeEvent<any>) => void;
|
||||
FormHelperTextProps?: ExtendedFormHelperTextProps;
|
||||
}
|
||||
|
||||
const DebounceAutocomplete: React.ComponentType<DebounceProps<
|
||||
|
@ -69,6 +71,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
|
|||
fetchChoices,
|
||||
onChange,
|
||||
onFetchMore,
|
||||
FormHelperTextProps,
|
||||
...rest
|
||||
} = props;
|
||||
const classes = useStyles(props);
|
||||
|
@ -178,6 +181,7 @@ const SingleAutocompleteSelectFieldComponent: React.FC<SingleAutocompleteSelectF
|
|||
error={error}
|
||||
disabled={disabled}
|
||||
helperText={helperText}
|
||||
FormHelperTextProps={FormHelperTextProps}
|
||||
label={label}
|
||||
fullWidth={true}
|
||||
/>
|
||||
|
|
29
src/components/SortableChip/SortableChip.stories.tsx
Normal file
29
src/components/SortableChip/SortableChip.stories.tsx
Normal 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>
|
||||
));
|
68
src/components/SortableChip/SortableChip.tsx
Normal file
68
src/components/SortableChip/SortableChip.tsx
Normal 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;
|
29
src/components/SortableChip/SortableHandle.tsx
Normal file
29
src/components/SortableChip/SortableHandle.tsx
Normal 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;
|
2
src/components/SortableChip/index.ts
Normal file
2
src/components/SortableChip/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./SortableChip";
|
||||
export * from "./SortableChip";
|
|
@ -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"
|
||||
/>
|
||||
));
|
84
src/components/SortableChipsField/SortableChipsField.tsx
Normal file
84
src/components/SortableChipsField/SortableChipsField.tsx
Normal 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;
|
5
src/components/SortableChipsField/SortableContainer.tsx
Normal file
5
src/components/SortableChipsField/SortableContainer.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { SortableContainer as SortableContainerHoc } from "react-sortable-hoc";
|
||||
|
||||
const SortableContainer = SortableContainerHoc(({ children }) => children);
|
||||
|
||||
export default SortableContainer;
|
2
src/components/SortableChipsField/index.ts
Normal file
2
src/components/SortableChipsField/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default } from "./SortableChipsField";
|
||||
export * from "./SortableChipsField";
|
|
@ -19,11 +19,14 @@ const useStyles = makeStyles(
|
|||
};
|
||||
|
||||
return {
|
||||
alertDot: {
|
||||
"&:before": { backgroundColor: yellow[500], ...dot }
|
||||
},
|
||||
errorDot: {
|
||||
"&:before": { backgroundColor: theme.palette.error.main, ...dot }
|
||||
},
|
||||
neutralDot: {
|
||||
"&:before": { backgroundColor: yellow[500], ...dot }
|
||||
"&:before": { backgroundColor: grey[300], ...dot }
|
||||
},
|
||||
root: {
|
||||
display: "inline-block",
|
||||
|
@ -35,9 +38,6 @@ const useStyles = makeStyles(
|
|||
},
|
||||
successDot: {
|
||||
"&:before": { backgroundColor: theme.palette.primary.main, ...dot }
|
||||
},
|
||||
unspecifiedDot: {
|
||||
"&:before": { backgroundColor: grey[500], ...dot }
|
||||
}
|
||||
};
|
||||
},
|
||||
|
@ -47,7 +47,7 @@ const useStyles = makeStyles(
|
|||
interface StatusLabelProps {
|
||||
className?: string;
|
||||
label: string | React.ReactNode;
|
||||
status: "success" | "neutral" | "unspecified" | "error" | string;
|
||||
status: "success" | "alert" | "neutral" | "error" | string;
|
||||
typographyProps?: TypographyProps;
|
||||
}
|
||||
|
||||
|
@ -62,8 +62,8 @@ const StatusLabel: React.FC<StatusLabelProps> = props => {
|
|||
[classes.root]: true,
|
||||
[className]: true,
|
||||
[classes.successDot]: status === "success",
|
||||
[classes.alertDot]: status === "alert",
|
||||
[classes.neutralDot]: status === "neutral",
|
||||
[classes.unspecifiedDot]: status === "unspecified",
|
||||
[classes.errorDot]: status === "error"
|
||||
})}
|
||||
>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue