Merge pull request #3 from mirumee/update

Update to newest mirumee/saleor:master
This commit is contained in:
Dominik Żegleń 2019-08-09 16:09:29 +02:00 committed by GitHub
commit 062bb48078
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
747 changed files with 51739 additions and 29407 deletions

View file

@ -1,73 +1,23 @@
dist: xenial dist: xenial
language: python language: node_js
sudo: false sudo: false
python: node_js:
- "3.6" - "11.15"
- "3.7"
branches: branches:
only: only:
- master - master
- /next\/.*/ - /next\/.*/
cache:
pip: true
directories:
- node_modules
- $HOME/.cache/pip
install:
- pip install -U pip setuptools wheel
- pip install tox-travis pytest-django-queries
- git clone https://github.com/NyanKiyoshi/pytest-django-queries-ci-tools.git --depth 1 ../queries-ci-tools
- >
if [ -n "$DJANGO" ]; then
nvm install 10 \
&& npm i \
&& npm run build-assets --production \
&& npm run build-emails
fi
script: script:
- > - npm run build
if [ -n "$DJANGO" ]; then - npm test -- --ci --coverage
npm run test
fi
- tox
env: env:
global: global:
- DATABASE_URL="postgres://postgres@localhost:5432/saleor" - APP_MOUNT_URI="/"
- SECRET_KEY="irrelevant" - API_URI="/graphql/"
- DIFF_RESULTS_BASE_URL="http://dhrwmpu5reeyd.cloudfront.net"
- QUERIES_RESULTS_PATH=/tmp/queries-results.json
matrix:
- DJANGO="2.2"
- DJANGO="master"
matrix:
fast_finish: true
include:
- env: TOXENV=black
python: "3.6"
- env: TOXENV=flake8
python: "3.6"
- env: TOXENV=check_gql_schema
python: "3.6"
- env: TOXENV=check_migrations
python: "3.6"
allow_failures:
- python: "3.6"
env: DJANGO="master"
- python: "3.7"
env: DJANGO="master"
services:
- postgresql
addons:
postgresql: 9.4
after_success: after_success:
- ../queries-ci-tools/handle-event.sh - codecov

View file

@ -1,517 +0,0 @@
# Changelog
All notable, unreleased changes to this project will be documented in this file. For the released changes, please visit the [Releases](https://github.com/mirumee/saleor/releases) page.
## [Unreleased Changeset: Attributes]
- The mutations `attributeCreate` and `attributeUpdate`:
- They no longer crash if no values were passed. - #4260 by @NyanKiyoshi
- They no longer accept the `productType` parameter and no longer return the attribute's `productType` in the root of the response; instead, you should query the attribute's `productType` manually. - #4260 by @NyanKiyoshi
- They now accept an optional `slug` input parameter for setting the internal representation of the attribute instead of always generating it. This allows users to have multiple attributes with the same name but that are different things or set of values. - #4260 by @NyanKiyoshi
- The `Attribute` model:
- They now always unique, instead of being unique only into a given product type. - #4260 by @NyanKiyoshi
- Attributes can now be associated to multiple product types. - #4266 by @NyanKiyoshi
- The `Attribute` model's `product_type` field was renamed to `product_types` (M2M related manager). - #4266 by @NyanKiyoshi
- Added `attributeAssign(...)` and `attributeUnassign(...)` mutations in the GraphQL API. - #4266 by @NyanKiyoshi
## [Unreleased]
- Fixed internal error when creating a checkout with a voucher code - #4292 by @NyanKiyoshi
- Add filter tab name as required - #4269 by @benekex2
- A few unused panels are now disabled by default from the debug toolbar; this should improve loading time when debugging - #4301 by @NyanKiyoshi
## 2.7.0
### API
- Create order only when payment is successful - #4154 by @NyanKiyoshi
- Order Events containing order lines or fulfillment lines now return the line object in the GraphQL API - #4114 by @NyanKiyoshi
- GraphQL now prints exceptions to stderr as well as returning them or not - #4148 by @NyanKiyoshi
- Refactored API resolvers to static methods with root typing - #4155 by @NyanKiyoshi
- Add phone validation in the GraphQL API to handle the library upgrade - #4156 by @NyanKiyoshi
### Core
- Add basic Gift Cards support in the backend - #4025 by @fowczarek
- Add the ability to sort products within a collection - #4123 by @NyanKiyoshi
- Implement customer events - #4094 by @NyanKiyoshi
- Merge "authorize" and "capture" operations - #4098 by @korycins, @NyanKiyoshi
- Separate the Django middlewares from the GraphQL API middlewares - #4102 by @NyanKiyoshi, #4186 by @cmiacz
### Dashboard 2.0
- Add navigation section - #4012 by @dominik-zeglen
- Add filtering on product list - #4193 by @dominik-zeglen
- Add filtering on orders list - #4237 by @dominik-zeglen
- Change input style and improve Storybook stories - #4115 by @dominik-zeglen
- Migrate deprecated fields in Dashboard 2.0 - #4121 by @benekex2
- Add multiple select checkbox - #4133, #4146 by @benekex2
- Rename menu items in Dashboard 2.0 - #4172 by @benekex2
- Category delete modal improvements - #4171 by @benekex2
- Close modals on click outside - #4236 - by @benekex2
- Use date localize hook in translations - #4202 by @dominik-zeglen
- Unify search API - #4200 by @dominik-zeglen
- Default default PAGINATE_BY - #4238 by @dominik-zeglen
- Create generic filtering interface - #4221 by @dominik-zeglen
- Add default state to rich text editor = #4281 by @dominik-zeglen
- Fix translation discard button - #4109 by @benekex2
- Fix draftail options and icons - #4132 by @benekex2
- Fix typos and messages in Dashboard 2.0 - #4168 by @benekex2
- Fix view all orders button - #4173 by @benekex2
- Fix visibility card view - #4198 by @benekex2
- Fix query refetch after selecting an object in list - #4272 by @dominik-zeglen
- Fix image selection in variants - #4270 by @benekex2
- Fix collection search - #4267 by @dominik-zeglen
- Fix quantity height in draft order edit - #4273 by @benekex2
- Fix checkbox clickable area size - #4280 by @dominik-zeglen
- Fix breaking object selection in menu section - #4282 by @dominik-zeglen
- Reset selected items when tab switch - #4268 by @benekex2
- Add attribute section - #4305 by @dominik-zeglen
### Other notable changes
- Add support for Google Cloud Storage - #4127 by @chetabahana
- Adding a nonexistent variant to checkout no longer crashes - #4166 by @NyanKiyoshi
- Disable storage of Celery results - #4169 by @NyanKiyoshi
- Disable polling in Playground - #4188 by @maarcingebala
- Cleanup code for updated function names and unused argument - #4090 by @jxltom
- Users can now add multiple "Add to Cart" forms in a single page - #4165 by @NyanKiyoshi
- Fix incorrect argument in `get_client_token` in Braintree integration - #4182 by @maarcingebala
- Fix resolving attribute values when transforming them to HStore - #4161 by @maarcingebala
- Fix wrong calculation of subtotal in cart page - #4145 by @korycins
- Fix margin calculations when product/variant price is set to zero - #4170 by @MahmoudRizk
- Fix applying discounts in checkout's subtotal calculation in API - #4192 by @maarcingebala
- Fix GATEWAYS_ENUM to always contain all implemented payment gateways - #4108 by @koradon
## 2.6.0
### API
- Add unified filtering interface in resolvers - #3952, #4078 by @korycins
- Add mutations for bulk actions - #3935, #3954, #3967, #3969, #3970 by @akjanik
- Add mutation for reordering menu items - #3958 by @NyanKiyoshi
- Optimize queries for single nodes - #3968 @NyanKiyoshi
- Refactor error handling in mutations #3891 by @maarcingebala & @akjanik
- Specify mutation permissions through Meta classes - #3980 by @NyanKiyoshi
- Unify pricing access in products and variants - #3948 by @NyanKiyoshi
- Use only_fields instead of exclude_fields in type definitions - #3940 by @michaljelonek
- Prefetch collections when getting sales of a bunch of products - #3961 by @NyanKiyoshi
- Remove unnecessary dedents from GraphQL schema so new Playground can work - #4045 by @salwator
- Restrict resolving payment by ID - #4009 @NyanKiyoshi
- Require `checkoutId` for updating checkout's shipping and billing address - #4074 by @jxltom
- Handle errors in `TokenVerify` mutation - #3981 by @fowczarek
- Unify argument names in types and resolvers - #3942 by @NyanKiyoshi
### Core
- Use Black as the default code formatting tool - #3852 by @krzysztofwolski and @NyanKiyoshi
- Dropped Python 3.5 support - #4028 by @korycins
- Rename Cart to Checkout - #3963 by @michaljelonek
- Use data classes to exchange data with payment gateways - #4028 by @korycins
- Refactor order events - #4018 by @NyanKiyoshi
### Dashboard 2.0
- Add bulk actions - #3955 by @dominik-zeglen
- Add user avatar management - #4030 by @benekex2
- Add navigation drawer support on mobile devices - #3839 by @benekex2
- Fix rendering validation errors in product form - #4024 by @benekex2
- Move dialog windows to query string rather than router paths - #3953 by @dominik-zeglen
- Update order events types - #4089 by @jxltom
- Code cleanup by replacing render props with react hooks - #4010 by @dominik-zeglen
### Other notable changes
- Add setting to enable Django Debug Toolbar - #3983 by @koradon
- Use newest GraphQL Playground - #3971 by @salwator
- Ensure adding to quantities in the checkout is respecting the limits - #4005 by @NyanKiyoshi
- Fix country area choices - #4008 by @fowczarek
- Fix price_range_as_dict function - #3999 by @zodiacfireworks
- Fix the product listing not showing in the voucher when there were products selected - #4062 by @NyanKiyoshi
- Fix crash in Dashboard 1.0 when updating an order address's phone number - #4061 by @NyanKiyoshi
- Reduce the time of tests execution by using dummy password hasher - #4083 by @korycins
- Set up explicit **hash** function - #3979 by @akjanik
- Unit tests use none as media root - #3975 by @korycins
- Update file field styles with materializecss template filter - #3998 by @zodiacfireworks
- New translations:
- Albanian
- Colombian Spanish
- Lithuanian
## 2.5.0
### API
- Add query to fetch draft orders - #3809 by @michaljelonek
- Add bulk delete mutations - #3838 by @michaljelonek
- Add `languageCode` enum to API - #3819 by @michaljelonek, #3854 by @jxltom
- Duplicate address instances in checkout mutations - #3866 by @pawelzar
- Restrict access to `orders` query for unauthorized users - #3861 by @pawelzar
- Support setting address as default in address mutations - #3787 by @jxltom
- Fix phone number validation in GraphQL when country prefix not given - #3905 by @patrys
- Report pretty stack traces in DEBUG mode - #3918 by @patrys
### Core
- Drop support for Django 2.1 and Django 1.11 (previous LTS) - #3929 by @patrys
- Fulfillment of digital products - #3868 by @korycins
- Introduce avatars for staff accounts - #3878 by @pawelzar
- Refactor the account avatars path from a relative to absolute - #3938 by @NyanKiyoshi
### Dashboard 2.0
- Add translations section - #3884 by @dominik-zeglen
- Add light/dark theme - #3856 by @dominik-zeglen
- Add customer's address book view - #3826 by @dominik-zeglen
- Add "Add variant" button on the variant details page = #3914 by @dominik-zeglen
- Add back arrows in "Configure" subsections - #3917 by @dominik-zeglen
- Display avatars in staff views - #3922 by @dominik-zeglen
- Prevent user from changing his own status and permissions - #3922 by @dominik-zeglen
- Fix crashing product create view - #3837, #3910 by @dominik-zeglen
- Fix layout in staff members details page - #3857 by @dominik-zeglen
- Fix unfocusing rich text editor - #3902 by @dominik-zeglen
- Improve accessibility - #3856 by @dominik-zeglen
### Other notable changes
- Improve user and staff management in dashboard 1.0 - #3781 by @jxltom
- Fix default product tax rate in Dashboard 1.0 - #3880 by @pawelzar
- Fix logo in docs - #3928 by @michaljelonek
- Fix name of logo file - #3867 by @jxltom
- Fix variants for juices in example data - #3926 by @michaljelonek
- Fix alignment of the cart dropdown on new bootstrap version - #3937 by @NyanKiyoshi
- Refactor the account avatars path from a relative to absolute - #3938 by @NyanKiyoshi
- New translations:
- Armenian
- Portuguese
- Swahili
- Thai
## 2.4.0
### API
- Add model translations support in GraphQL API - #3789 by @michaljelonek
- Add mutations to manage addresses for authenticated customers - #3772 by @Kwaidan00, @maarcingebala
- Add mutation to apply vouchers in checkout - #3739 by @Kwaidan00
- Add thumbnail field to `OrderLine` type - #3737 by @michaljelonek
- Add a query to fetch order by token - #3740 by @michaljelonek
- Add city choices and city area type to address validator API - #3788 by @jxltom
- Fix access to unpublished objects in API - #3724 by @Kwaidan00
- Fix bug where errors are not returned when creating fulfillment with a non-existent order line - #3777 by @jxltom
- Fix `productCreate` mutation when no product type was provided - #3804 by @michaljelonek
- Enable database search in products query - #3736 by @michaljelonek
- Use authenticated user's email as default email in creating checkout - #3726 by @jxltom
- Generate voucher code if it wasn't provided in mutation - #3717 by @Kwaidan00
- Improve limitation of vouchers by country - #3707 by @michaljelonek
- Only include canceled fulfillments for staff in fulfillment API - #3778 by @jxltom
- Support setting address as when creating customer address #3782 by @jxltom
- Fix generating slug from title - #3816 by @maarcingebala
- Add `variant` field to `OrderLine` type - #3820 by @maarcingebala
### Core
- Add JSON fields to store rich-text content - #3756 by @michaljelonek
- Add function to recalculate total order weight - #3755 by @Kwaidan00, @maarcingebala
- Unify cart creation logic in API and Django views - #3761, #3790 by @maarcingebala
- Unify payment creation logic in API and Django views - #3715 by @maarcingebala
- Support partially charged and refunded payments - #3735 by @jxltom
- Support partial fulfillment of ordered items - #3754 by @jxltom
- Fix applying discounts when a sale has no end date - #3595 by @cprinos
### Dashboard 2.0
- Add "Discounts" section - #3654 by @dominik-zeglen
- Add "Pages" section; introduce Draftail WYSIWYG editor - #3751 by @dominik-zeglen
- Add "Shipping Methods" section - #3770 by @dominik-zeglen
- Add support for date and datetime components - #3708 by @dominik-zeglen
- Restyle app layout - #3811 by @dominik-zeglen
### Other notable changes
- Unify model field names related to models' public access - `publication_date` and `is_published` - #3706 by @michaljelonek
- Improve filter orders by payment status - #3749 @jxltom
- Refactor translations in emails - #3701 by @Kwaidan00
- Use exact image versions in docker-compose - #3742 by @ashishnitinpatil
- Sort order payment and history in descending order - #3747 by @jxltom
- Disable style-loader in dev mode - #3720 by @jxltom
- Add ordering to shipping method - #3806 by @michaljelonek
- Add missing type definition for dashboard 2.0 - #3776 by @jxltom
- Add header and footer for checkout success pages #3752 by @jxltom
- Add instructions for using local assets in Docker - #3723 by @michaljelonek
- Update S3 deployment documentation to include CORS configuration note - #3743 by @NyanKiyoshi
- Fix missing migrations for is_published field of product and page model - #3757 by @jxltom
- Fix problem with l10n in Braintree payment gateway template - #3691 by @Kwaidan00
- Fix bug where payment is not filtered from active ones when creating payment - #3732 by @jxltom
- Fix incorrect cart badge location - #3786 by @jxltom
- Fix storefront styles after bootstrap is updated to 4.3.1 - #3753 by @jxltom
- Fix logo size in different browser and devices with different sizes - #3722 by @jxltom
- Rename dumpdata file `db.json` to `populatedb_data.json` - #3810 by @maarcingebala
- Prefetch collections for product availability - #3813 by @michaljelonek
- Bump django-graphql-jwt - #3814 by @michaljelonek
- Fix generating slug from title - #3816 by @maarcingebala
- New translations:
- Estonian
- Indonesian
## 2.3.1
- Fix access to private variant fields in API - #3773 by maarcingebala
- Limit access of quantity and allocated quantity to staff in GraphQL API #3780 by @jxltom
## 2.3.0
### API
- Return user's last checkout in the `User` type - #3578 by @fowczarek
- Automatically assign checkout to the logged in user - #3587 by @fowczarek
- Expose `chargeTaxesOnShipping` field in the `Shop` type - #3603 by @fowczarek
- Expose list of enabled payment gateways - #3639 by @fowczarek
- Validate uploaded files in a unified way - #3633 by @fowczarek
- Add mutation to trigger fetching tax rates - #3622 by @fowczarek
- Use USERNAME_FIELD instead of hard-code email field when resolving user - #3577 by @jxltom
- Require variant and quantity fields in `CheckoutLineInput` type - #3592 by @jxltom
- Preserve order of nodes in `get_nodes_or_error` function - #3632 by @jxltom
- Add list mutations for `Voucher` and `Sale` models - #3669 by @michaljelonek
- Use proper type for countries in `Voucher` type - #3664 by @michaljelonek
- Require email in when creating checkout in API - #3667 by @michaljelonek
- Unify returning errors in the `tokenCreate` mutation - #3666 by @michaljelonek
- Use `Date` field in Sale/Voucher inputs - #3672 by @michaljelonek
- Refactor checkout mutations - #3610 by @fowczarek
- Refactor `clean_instance`, so it does not returns errors anymore - #3597 by @akjanik
- Handle GraphqQL syntax errors - #3576 by @jxltom
### Core
- Refactor payments architecture - #3519 by @michaljelonek
- Improve Docker and `docker-compose` configuration - #3657 by @michaljelonek
- Allow setting payment status manually for dummy gateway in Storefront 1.0 - #3648 by @jxltom
- Infer default transaction kind from operation type - #3646 by @jxltom
- Get correct payment status for order without any payments - #3605 by @jxltom
- Add default ordering by `id` for `CartLine` model - #3593 by @jxltom
- Fix "set password" email sent to customer created in the dashboard - #3688 by @Kwaidan00
### Dashboard 2.0
- Add taxes section - #3622 by @dominik-zeglen
- Add drag'n'drop image upload - #3611 by @dominik-zeglen
- Unify grid handling - #3520 by @dominik-zeglen
- Add component generator - #3670 by @dominik-zeglen
- Throw Typescript errors while snapshotting - #3611 by @dominik-zeglen
- Simplify mutation's error checking - #3589 by @dominik-zeglen
- Fix order cancelling - #3624 by @dominik-zeglen
- Fix logo placement - #3602 by @dominik-zeglen
### Other notable changes
- Register Celery task for updating exchange rates - #3599 by @jxltom
- Fix handling different attributes with the same slug - #3626 by @jxltom
- Add missing migrations for tax rate choices - #3629 by @jxltom
- Fix `TypeError` on calling `get_client_token` - #3660 by @michaljelonek
- Make shipping required as default when creating product types - #3655 by @jxltom
- Display payment status on customer's account page in Storefront 1.0 - #3637 by @jxltom
- Make order fields sequence in Dashboard 1.0 same as in Dashboard 2.0 - #3606 by @jxltom
- Fix returning products for homepage for the currently viewing user - #3598 by @jxltom
- Allow filtering payments by status in Dashboard 1.0 - #3608 by @jxltom
- Fix typo in the definition of order status - #3649 by @jxltom
- Add margin for order notes section - #3650 by @jxltom
- Fix logo position - #3609, #3616 by @jxltom
- Storefront visual improvements - #3696 by @piotrgrundas
- Fix product list price filter - #3697 by @Kwaidan00
- Redirect to success page after successful payment - #3693 by @Kwaidan00
## 2.2.0
### API
- Use `PermissionEnum` as input parameter type for `permissions` field - #3434 by @maarcingebala
- Add "authorize" and "charge" mutations for payments - #3426 by @jxltom
- Add alt text to product thumbnails and background images of collections and categories - #3429 by @fowczarek
- Fix passing decimal arguments = #3457 by @fowczarek
- Allow sorting products by the update date - #3470 by @jxltom
- Validate and clear the shipping method in draft order mutations - #3472 by @fowczarek
- Change tax rate field to choice field - #3478 by @fowczarek
- Allow filtering attributes by collections - #3508 by @maarcingebala
- Resolve to `None` when empty object ID was passed as mutation argument - #3497 by @maarcingebala
- Change `errors` field type from [Error] to [Error!] - #3489 by @fowczarek
- Support creating default variant for product types that don't use multiple variants - #3505 by @fowczarek
- Validate SKU when creating a default variant - #3555 by @fowczarek
- Extract enums to separate files - #3523 by @maarcingebala
### Core
- Add Stripe payment gateway - #3408 by @jxltom
- Add `first_name` and `last_name` fields to the `User` model - #3101 by @fowczarek
- Improve several payment validations - #3418 by @jxltom
- Optimize payments related database queries - #3455 by @jxltom
- Add publication date to collections - #3369 by @k-brk
- Fix hard-coded site name in order PDFs - #3526 by @NyanKiyoshi
- Update favicons to the new style - #3483 by @dominik-zeglen
- Fix migrations for default currency - #3235 by @bykof
- Remove Elasticsearch from `docker-compose.yml` - #3482 by @maarcingebala
- Resort imports in tests - #3471 by @jxltom
- Fix the no shipping orders payment crash on Stripe - #3550 by @NyanKiyoshi
- Bump backend dependencies - #3557 by @maarcingebala. This PR removes security issue CVE-2019-3498 which was present in Django 2.1.4. Saleor however wasn't vulnerable to this issue as it doesn't use the affected `django.views.defaults.page_not_found()` view.
- Generate random data using the default currency - #3512 by @stephenmoloney
- New translations:
- Catalan
- Serbian
### Dashboard 2.0
- Restyle product selection dialogs - #3499 by @dominik-zeglen, @maarcingebala
- Fix minor visual bugs in Dashboard 2.0 - #3433 by @dominik-zeglen
- Display warning if order draft has missing data - #3431 by @dominik-zeglen
- Add description field to collections - #3435 by @dominik-zeglen
- Add query batching - #3443 by @dominik-zeglen
- Use autocomplete fields in country selection - #3443 by @dominik-zeglen
- Add alt text to categories and collections - #3461 by @dominik-zeglen
- Use first and last name of a customer or staff member in UI - #3247 by @Bonifacy1, @dominik-zeglen
- Show error page if an object was not found - #3463 by @dominik-zeglen
- Fix simple product's inventory data saving bug - #3474 by @dominik-zeglen
- Replace `thumbnailUrl` with `thumbnail { url }` - #3484 by @dominik-zeglen
- Change "Feature on Homepage" switch behavior - #3481 by @dominik-zeglen
- Expand payment section in order view - #3502 by @dominik-zeglen
- Change TypeScript loader to speed up the build process - #3545 by @patrys
### Bugfixes
- Do not show `Pay For Order` if order is partly paid since partial payment is not supported - #3398 by @jxltom
- Fix attribute filters in the products category view - #3535 by @fowczarek
- Fix storybook dependencies conflict - #3544 by @dominik-zeglen
## 2.1.0
### API
- Change selected connection fields to lists - #3307 by @fowczarek
- Require pagination in connections - #3352 by @maarcingebala
- Replace Graphene view with a custom one - #3263 by @patrys
- Change `sortBy` parameter to use enum type - #3345 by @fowczarek
- Add `me` query to fetch data of a logged-in user - #3202, #3316 by @fowczarek
- Add `canFinalize` field to the Order type - #3356 by @fowczarek
- Extract resolvers and mutations to separate files - #3248 by @fowczarek
- Add VAT tax rates field to country - #3392 by @michaljelonek
- Allow creating orders without users - #3396 by @fowczarek
### Core
- Add Razorpay payment gatway - #3205 by @NyanKiyoshi
- Use standard tax rate as a default tax rate value - #3340 by @fowczarek
- Add description field to the Collection model - #3275 by @fowczarek
- Enforce the POST method on VAT rates fetching - #3337 by @NyanKiyoshi
- Generate thumbnails for category/collection background images - #3270 by @NyanKiyoshi
- Add warm-up support in product image creation mutation - #3276 by @NyanKiyoshi
- Fix error in the `populatedb` script when running it not from the project root - #3272 by @NyanKiyoshi
- Make Webpack rebuilds fast - #3290 by @patrys
- Skip installing Chromium to make deployment faster - #3227 by @jxltom
- Add default test runner - #3258 by @jxltom
- Add Transifex client to Pipfile - #3321 by @jxltom
- Remove additional pytest arguments in tox - #3338 by @jxltom
- Remove test warnings - #3339 by @jxltom
- Remove runtime warning when product has discount - #3310 by @jxltom
- Remove `django-graphene-jwt` warnings - #3228 by @jxltom
- Disable deprecated warnings - #3229 by @jxltom
- Add `AWS_S3_ENDPOINT_URL` setting to support DigitalOcean spaces. - #3281 by @hairychris
- Add `.gitattributes` file to hide diffs for generated files on Github - #3055 by @NyanKiyoshi
- Add database sequence reset to `populatedb` - #3406 by @michaljelonek
- Get authorized amount from succeeded auth transactions - #3417 by @jxltom
- Resort imports by `isort` - #3412 by @jxltom
### Dashboard 2.0
- Add confirmation modal when leaving view with unsaved changes - #3375 by @dominik-zeglen
- Add dialog loading and error states - #3359 by @dominik-zeglen
- Split paths and urls - #3350 by @dominik-zeglen
- Derive state from props in forms - #3360 by @dominik-zeglen
- Apply debounce to autocomplete fields - #3351 by @dominik-zeglen
- Use Apollo signatures - #3353 by @dominik-zeglen
- Add order note field in the order details view - #3346 by @dominik-zeglen
- Add app-wide progress bar - #3312 by @dominik-zeglen
- Ensure that all queries are built on top of TypedQuery - #3309 by @dominik-zeglen
- Close modal windows automatically - #3296 by @dominik-zeglen
- Move URLs to separate files - #3295 by @dominik-zeglen
- Add basic filters for products and orders list - #3237 by @Bonifacy1
- Fetch default currency from API - #3280 by @dominik-zeglen
- Add `displayName` property to components - #3238 by @Bonifacy1
- Add window titles - #3279 by @dominik-zeglen
- Add paginator component - #3265 by @dominik-zeglen
- Update Material UI to 3.6 - #3387 by @patrys
- Upgrade React, Apollo, Webpack and Babel - #3393 by @patrys
- Add pagination for required connections - #3411 by @dominik-zeglen
### Bugfixes
- Fix language codes - #3311 by @jxltom
- Fix resolving empty attributes list - #3293 by @maarcingebala
- Fix range filters not being applied - #3385 by @michaljelonek
- Remove timeout for updating image height - #3344 by @jxltom
- Return error if checkout was not found - #3289 by @maarcingebala
- Solve an auto-resize conflict between Materialize and medium-editor - #3367 by @adonig
- Fix calls to `ngettext_lazy` - #3380 by @patrys
- Filter preauthorized order from succeeded transactions - #3399 by @jxltom
- Fix incorrect country code in fixtures - #3349 by @bingimar
- Fix updating background image of a collection - #3362 by @fowczarek & @dominik-zeglen
### Docs
- Document settings related to generating thumbnails on demand - #3329 by @NyanKiyoshi
- Improve documentation for Heroku deployment - #3170 by @raybesiga
- Update documentation on Docker deployment - #3326 by @jxltom
- Document payment gateway configuration - #3376 by @NyanKiyoshi
## 2.0.0
### API
- Add mutation to delete a customer; add `isActive` field in `customerUpdate` mutation - #3177 by @maarcingebala
- Add mutations to manage authorization keys - #3082 by @maarcingebala
- Add queries for dashboard homepage - #3146 by @maarcingebala
- Allows user to unset homepage collection - #3140 by @oldPadavan
- Use enums as permission codes - #3095 by @the-bionic
- Return absolute image URLs - #3182 by @maarcingebala
- Add `backgroundImage` field to `CategoryInput` - #3153 by @oldPadavan
- Add `dateJoined` and `lastLogin` fields in `User` type - #3169 by @maarcingebala
- Separate `parent` input field from `CategoryInput` - #3150 by @akjanik
- Remove duplicated field in Order type - #3180 by @maarcingebala
- Handle empty `backgroundImage` field in API - #3159 by @maarcingebala
- Generate name-based slug in collection mutations - #3145 by @akjanik
- Remove products field from `collectionUpdate` mutation - #3141 by @oldPadavan
- Change `items` field in `Menu` type from connection to list - #3032 by @oldPadavan
- Make `Meta.description` required in `BaseMutation` - #3034 by @oldPadavan
- Apply `textwrap.dedent` to GraphQL descriptions - #3167 by @fowczarek
### Dashboard 2.0
- Add collection management - #3135 by @dominik-zeglen
- Add customer management - #3176 by @dominik-zeglen
- Add homepage view - #3155, #3178 by @Bonifacy1 and @dominik-zeglen
- Add product type management - #3052 by @dominik-zeglen
- Add site settings management - #3071 by @dominik-zeglen
- Escape node IDs in URLs - #3115 by @dominik-zeglen
- Restyle categories section - #3072 by @Bonifacy1
### Other
- Change relation between `ProductType` and `Attribute` models - #3097 by @maarcingebala
- Remove `quantity-allocated` generation in `populatedb` script - #3084 by @MartinSeibert
- Handle `Money` serialization - #3131 by @Pacu2
- Do not collect unnecessary static files - #3050 by @jxltom
- Remove host mounted volume in `docker-compose` - #3091 by @tiangolo
- Remove custom services names in `docker-compose` - #3092 by @tiangolo
- Replace COUNTRIES with countries.countries - #3079 by @neeraj1909
- Installing dev packages in docker since tests are needed - #3078 by @jxltom
- Remove comparing string in address-form-panel template - #3074 by @tomcio1205
- Move updating variant names to a Celery task - #3189 by @fowczarek
### Bugfixes
- Fix typo in `clean_input` method - #3100 by @the-bionic
- Fix typo in `ShippingMethod` model - #3099 by @the-bionic
- Remove duplicated variable declaration - #3094 by @the-bionic
### Docs
- Add createdb note to getting started for Windows - #3106 by @ajostergaard
- Update docs on pipenv - #3045 by @jxltom

View file

@ -1,14 +1,11 @@
module.exports = { module.exports = {
client: { client: {
addTypename: true, addTypename: true,
includes: [ includes: ["src/**/*.ts", "src/**/*.tsx"],
'saleor/static/dashboard-next/**/*.ts', name: "dashboard",
'saleor/static/dashboard-next/**/*.tsx'
],
name: 'storefront',
service: { service: {
localSchemaFile: 'saleor/graphql/schema.graphql', endpoint: "localhost:8000/graphql/",
name: 'saleor' name: "saleor"
} }
} }
}; };

View file

@ -0,0 +1,4 @@
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.7216 5.23914L1.90735e-06 5.23914L5.27835 -4.00543e-05L32 -4.00543e-05L26.7216 5.23914Z" fill="#19C3BE"/>
<path d="M15.3359 32.8964C21.9014 32.8964 23.5522 30.3032 23.5522 26.7507C23.5522 22.3812 19.5378 21.5641 16.1988 21.0313C13.6476 20.605 12.4846 20.3918 12.4846 18.8643C12.4846 17.7275 13.4975 17.3012 15.1483 17.3012C17.1742 17.3012 17.737 17.8696 17.9246 19.5392L23.2521 18.7577C22.8019 15.1697 20.7384 13.358 15.2984 13.358C9.33309 13.358 7.23211 15.7026 7.23211 19.4327C7.19459 24.264 11.8468 24.7613 15.2984 25.3297C17.4369 25.7205 18.2622 26.0757 18.2622 27.4612C18.2622 28.598 17.5494 29.0953 15.4109 29.0953C13.0473 29.0953 12.3345 28.7046 12.0343 26.8218L6.85693 27.5678C7.34466 31.3689 9.63322 32.8964 15.3359 32.8964Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 865 B

View file

@ -1,4 +1,4 @@
<svg viewBox="0 0 119 40" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.29798 0L0 6.36371H31.8841L38.1818 0H6.29798Z" fill="#13BEBB"/> <path d="M6.29798 0L0 6.36371H31.8841L38.1818 0H6.29798Z" fill="#13BEBB"/>
<path d="M18.3033 39.9567C26.1372 39.9567 28.1071 36.8068 28.1071 32.4921C28.1071 27.1847 23.3171 26.1924 19.333 25.545C16.2888 25.0271 14.9011 24.7683 14.9011 22.913C14.9011 21.5323 16.1097 21.0144 18.0795 21.0144C20.4969 21.0144 21.1683 21.7049 21.3923 23.7328L27.749 22.7835C27.2115 18.4255 24.7495 16.2249 18.2585 16.2249C11.141 16.2249 8.63409 19.0727 8.63409 23.6033C8.58946 29.4717 14.1403 30.0756 18.2585 30.7658C20.81 31.2409 21.7952 31.6719 21.7952 33.3549C21.7952 34.7359 20.9447 35.34 18.3928 35.34C15.5726 35.34 14.7221 34.8653 14.364 32.5783L8.18652 33.4844C8.7684 38.1014 11.4991 39.9567 18.3033 39.9567Z" fill="white"/> <path d="M18.3033 39.9567C26.1372 39.9567 28.1071 36.8068 28.1071 32.4921C28.1071 27.1847 23.3171 26.1924 19.333 25.545C16.2888 25.0271 14.9011 24.7683 14.9011 22.913C14.9011 21.5323 16.1097 21.0144 18.0795 21.0144C20.4969 21.0144 21.1683 21.7049 21.3923 23.7328L27.749 22.7835C27.2115 18.4255 24.7495 16.2249 18.2585 16.2249C11.141 16.2249 8.63409 19.0727 8.63409 23.6033C8.58946 29.4717 14.1403 30.0756 18.2585 30.7658C20.81 31.2409 21.7952 31.6719 21.7952 33.3549C21.7952 34.7359 20.9447 35.34 18.3928 35.34C15.5726 35.34 14.7221 34.8653 14.364 32.5783L8.18652 33.4844C8.7684 38.1014 11.4991 39.9567 18.3033 39.9567Z" fill="white"/>
<path d="M38.794 35.2108C35.7944 35.2108 34.7204 34.5635 34.7204 32.4061C34.7204 30.4644 35.7944 29.6877 39.0177 29.6877H42.2408V31.9315C42.2408 34.0889 41.1665 35.2108 38.794 35.2108ZM36.0184 39.9571C39.5101 39.9571 41.0771 38.8351 42.1962 37.1954V39.655H48.5078V24.0352C48.5078 18.8141 46.4933 16.1821 39.5101 16.1821C32.7505 16.1821 30.3782 18.0805 29.438 22.7406L35.7052 23.69C36.1079 21.7052 36.69 21.1009 39.2864 21.1009C41.7932 21.1009 42.1962 22.1365 42.1962 23.8626V25.4592H39.2415C31.2287 25.4592 28.4978 27.8755 28.4978 32.7943C28.4978 37.6269 30.9601 39.9571 36.0184 39.9571Z" fill="white"/> <path d="M38.794 35.2108C35.7944 35.2108 34.7204 34.5635 34.7204 32.4061C34.7204 30.4644 35.7944 29.6877 39.0177 29.6877H42.2408V31.9315C42.2408 34.0889 41.1665 35.2108 38.794 35.2108ZM36.0184 39.9571C39.5101 39.9571 41.0771 38.8351 42.1962 37.1954V39.655H48.5078V24.0352C48.5078 18.8141 46.4933 16.1821 39.5101 16.1821C32.7505 16.1821 30.3782 18.0805 29.438 22.7406L35.7052 23.69C36.1079 21.7052 36.69 21.1009 39.2864 21.1009C41.7932 21.1009 42.1962 22.1365 42.1962 23.8626V25.4592H39.2415C31.2287 25.4592 28.4978 27.8755 28.4978 32.7943C28.4978 37.6269 30.9601 39.9571 36.0184 39.9571Z" fill="white"/>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,3 @@
<svg width="17" height="8" viewBox="0 0 17 8" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2626 4.96719L-1.32568e-07 4.96719L-2.17123e-07 3.03281L13.2626 3.03281L11.5821 1.36782L12.9627 -5.66617e-07L17 4L12.9627 8L11.5821 6.63218L13.2626 4.96719Z" />
</svg>

After

Width:  |  Height:  |  Size: 302 B

View file

@ -0,0 +1,3 @@
<svg width="126" height="126" viewBox="0 0 126 126" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.1484 88.5938V100.413L5.90625 116.656V5.90625H22.1484V19.6875H15.75V25.5938H22.1484V35.4375H15.75V41.3438H22.1484V51.1875H15.75V57.0938H22.1484V66.9375H15.75V72.8438H22.1484V82.6875H15.75V88.5938H22.1484ZM28.0547 0H0V126H125.754V98.6836H28.0547V0ZM26.3248 104.59L10.8209 120.094H119.848V104.59H107.789V110.988H101.883V104.59H92.0391V110.988H86.1328V104.59H76.2891V110.988H70.3828V104.59H60.5391V110.988H54.6328V104.59H44.7891V110.988H38.8828V104.59H26.3248ZM77.4305 8.20797L117.141 21.4779V74.8355L77.4306 88.7689L37.4062 74.8413V21.4723L77.4305 8.20797ZM43.3125 27.6624V70.6429L74.4735 81.4862V37.7312L43.3125 27.6624ZM80.3797 37.7257V81.4748L111.234 70.6486V27.6728L80.3797 37.7257ZM104.764 23.5692L77.4228 32.4772L49.854 23.5691L77.4227 14.4327L104.764 23.5692Z" fill="#3D3D3D"/>
</svg>

After

Width:  |  Height:  |  Size: 942 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="126" height="126" viewBox="0 0 126 126" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.966 18.0156L60.1699 17.079L62.5078 12.6506L64.8458 17.079L70.0496 18.0156L66.2335 21.8424L67.0007 26.8435L62.5455 24.5459L57.9645 26.8428L58.8477 21.9081L54.966 18.0156ZM42.8203 14.2005L56.2907 11.776L62.5078 0L68.7249 11.776L82.1953 14.2005L72.5243 23.8984L74.5966 37.4062L62.5078 31.1719L50.0736 37.4062L52.4914 23.8984L42.8203 14.2005ZM19.0363 31.3047L24.2402 30.3681L26.5781 25.9397L28.9161 30.3681L34.12 31.3047L30.3038 35.1315L31.0711 40.1326L26.6158 37.835L22.0348 40.1319L22.918 35.1972L19.0363 31.3047ZM6.89062 27.4896L20.361 25.0651L26.5781 13.2891L32.7952 25.0651L46.2656 27.4896L36.5946 37.1875L38.6669 50.6953L26.5781 44.4609L14.1439 50.6953L16.5617 37.1875L6.89062 27.4896ZM15.7227 86.1328H36.4219L34.6279 92.0391H20.2071L10.8281 126H4.42969L15.7227 86.1328ZM110.277 86.1328H89.5781L91.372 92.0391H105.793L115.172 126H121.57L110.277 86.1328ZM94.623 30.3681L89.4191 31.3047L93.3008 35.1972L92.4176 40.1319L96.9986 37.835L101.454 40.1326L100.687 35.1315L104.503 31.3047L99.2989 30.3681L96.9609 25.9397L94.623 30.3681ZM90.7438 25.0651L77.2734 27.4896L86.9445 37.1875L84.5267 50.6953L96.9609 44.4609L109.05 50.6953L106.977 37.1875L116.648 27.4896L103.178 25.0651L96.9609 13.2891L90.7438 25.0651ZM31.0078 126H94.2539L84.1641 81.2109H41.3438L31.0078 126ZM38.4323 120.094H86.8691L79.4403 87.1172H46.0422L38.4323 120.094ZM71.3672 62.0156C71.3672 66.9085 67.4007 70.875 62.5078 70.875C57.6149 70.875 53.6484 66.9085 53.6484 62.0156C53.6484 57.1227 57.6149 53.1562 62.5078 53.1562C67.4007 53.1562 71.3672 57.1227 71.3672 62.0156ZM77.2734 62.0156C77.2734 70.1705 70.6626 76.7812 62.5078 76.7812C54.353 76.7812 47.7422 70.1705 47.7422 62.0156C47.7422 53.8608 54.353 47.25 62.5078 47.25C70.6626 47.25 77.2734 53.8608 77.2734 62.0156ZM96.9609 75.3047C101.854 75.3047 105.82 71.3382 105.82 66.4453C105.82 61.5524 101.854 57.5859 96.9609 57.5859C92.068 57.5859 88.1016 61.5524 88.1016 66.4453C88.1016 71.3382 92.068 75.3047 96.9609 75.3047ZM96.9609 81.2109C105.116 81.2109 111.727 74.6001 111.727 66.4453C111.727 58.2905 105.116 51.6797 96.9609 51.6797C88.8061 51.6797 82.1953 58.2905 82.1953 66.4453C82.1953 74.6001 88.8061 81.2109 96.9609 81.2109ZM36.9141 68.9062C36.9141 73.7991 32.9476 77.7656 28.0547 77.7656C23.1618 77.7656 19.1953 73.7991 19.1953 68.9062C19.1953 64.0134 23.1618 60.0469 28.0547 60.0469C32.9476 60.0469 36.9141 64.0134 36.9141 68.9062ZM42.8203 68.9062C42.8203 77.0611 36.2095 83.6719 28.0547 83.6719C19.8999 83.6719 13.2891 77.0611 13.2891 68.9062C13.2891 60.7514 19.8999 54.1406 28.0547 54.1406C36.2095 54.1406 42.8203 60.7514 42.8203 68.9062Z" fill="#3D3D3D"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,3 @@
<svg width="126" height="126" viewBox="0 0 126 126" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 70.6383L59.0625 126L126 51.9998V14.7656L110.742 0H72.8438L0 70.6383ZM12.8816 66.3739L8.55895 70.5657L58.7154 117.579L62.9906 112.853L12.8816 66.3739ZM66.953 108.473L17.1276 62.2565L75.2372 5.90625H108.352L120.094 17.269V49.7249L66.953 108.473ZM61.5234 89.5781V35.4375H67.4297V89.5781H61.5234ZM105.328 25.1016C105.328 28.0917 102.904 30.5156 99.9141 30.5156C96.924 30.5156 94.5 28.0917 94.5 25.1016C94.5 22.1115 96.924 19.6875 99.9141 19.6875C102.904 19.6875 105.328 22.1115 105.328 25.1016ZM111.234 25.1016C111.234 31.3536 106.166 36.4219 99.9141 36.4219C93.662 36.4219 88.5938 31.3536 88.5938 25.1016C88.5938 18.8495 93.662 13.7812 99.9141 13.7812C106.166 13.7812 111.234 18.8495 111.234 25.1016ZM80.9648 66.9375C83.2754 66.9375 85.1484 65.0644 85.1484 62.7539C85.1484 60.4434 83.2754 58.5703 80.9648 58.5703C78.6543 58.5703 76.7812 60.4434 76.7812 62.7539C76.7812 65.0644 78.6543 66.9375 80.9648 66.9375ZM80.9648 72.8438C86.5373 72.8438 91.0547 68.3264 91.0547 62.7539C91.0547 57.1814 86.5373 52.6641 80.9648 52.6641C75.3924 52.6641 70.875 57.1814 70.875 62.7539C70.875 68.3264 75.3924 72.8438 80.9648 72.8438ZM51.6797 62.7539C51.6797 65.0644 49.8066 66.9375 47.4961 66.9375C45.1856 66.9375 43.3125 65.0644 43.3125 62.7539C43.3125 60.4434 45.1856 58.5703 47.4961 58.5703C49.8066 58.5703 51.6797 60.4434 51.6797 62.7539ZM57.5859 62.7539C57.5859 68.3264 53.0686 72.8438 47.4961 72.8438C41.9236 72.8438 37.4062 68.3264 37.4062 62.7539C37.4062 57.1814 41.9236 52.6641 47.4961 52.6641C53.0686 52.6641 57.5859 57.1814 57.5859 62.7539Z" fill="#3D3D3D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,3 @@
<svg width="111" height="126" viewBox="0 0 111 126" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M104.219 5.90625H6.27344V28.5469H104.219V5.90625ZM6.27344 0H0.367188V5.90625V28.5469V34.4531H6.27344H104.219H110.125V28.5469V5.90625V0H104.219H6.27344ZM45.1562 47.7422H6.27344V120.094H45.1562V47.7422ZM6.27344 41.8359H0.367188V47.7422V120.094V126H6.27344H45.1562H51.0625V120.094V47.7422V41.8359H45.1562H6.27344ZM65.3359 47.7422H104.219V59.0625H65.3359V47.7422ZM59.4297 41.8359H65.3359H104.219H110.125V47.7422V59.0625V64.9688H104.219H65.3359H59.4297V59.0625V47.7422V41.8359ZM104.219 108.773H65.3359V120.094H104.219V108.773ZM65.3359 102.867H59.4297V108.773V120.094V126H65.3359H104.219H110.125V120.094V108.773V102.867H104.219H65.3359ZM65.3359 78.2578H104.219V89.5781H65.3359V78.2578ZM59.4297 72.3516H65.3359H104.219H110.125V78.2578V89.5781V95.4844H104.219H65.3359H59.4297V89.5781V78.2578V72.3516Z" fill="#3D3D3D"/>
</svg>

After

Width:  |  Height:  |  Size: 967 B

View file

@ -0,0 +1,3 @@
<svg width="126" height="126" viewBox="0 0 126 126" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M105.328 0.492188H16.7344V125.508H105.328V107.789H99.4219V119.602H22.6406V6.39844H99.4219V68.9062H105.328V0.492188ZM91.3008 102.86L125.135 69.0257L120.959 64.8493L91.3008 94.5072L79.8538 83.0602L75.6774 87.2366L91.3008 102.86ZM32.4844 54.6328H38.3906V60.5391H32.4844V54.6328ZM89.5781 54.6328H45.2812V60.5391H89.5781V54.6328ZM32.4844 42.8203H38.3906V48.7266H32.4844V42.8203ZM89.5781 42.8203H45.2812V48.7266H89.5781V42.8203ZM32.4844 31.0078H38.3906V36.9141H32.4844V31.0078ZM89.5781 31.0078H45.2812V36.9141H89.5781V31.0078ZM32.4844 19.1953H38.3906V25.1016H32.4844V19.1953ZM89.5781 19.1953H45.2812V25.1016H89.5781V19.1953Z" fill="#3D3D3D"/>
</svg>

After

Width:  |  Height:  |  Size: 793 B

View file

@ -0,0 +1,3 @@
<svg width="126" height="126" viewBox="0 0 126 126" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.7001 116.365L16.2586 97.9453H35.3012C37.679 103.982 40.6312 110.258 44.1417 116.908C39.855 115.408 35.8027 113.412 32.0522 110.987L29.628 109.42L9.7001 116.365ZM60.0469 120.019V97.9453H41.6706C44.3861 104.516 47.8275 111.48 52.0086 119.038C54.6277 119.548 57.3114 119.88 60.0469 120.019ZM65.9531 97.9453H84.4393C81.7269 104.508 78.2902 111.464 74.1154 119.012C71.4563 119.537 68.7309 119.877 65.9531 120.019V97.9453ZM65.9531 92.0391V65.9531H92.1007C91.7928 74.3536 90.0135 82.8472 86.7212 92.0391H65.9531ZM60.0469 60.0469V33.2227H39.8646C36.3428 42.7476 34.4238 51.4762 34.0351 60.0469H60.0469ZM65.9531 33.2227V60.0469H92.0748C91.6862 51.4762 89.7671 42.7476 86.2453 33.2227H65.9531ZM65.9531 27.3164V5.98131C68.6242 6.1174 71.2468 6.43707 73.8086 6.92794C77.8557 14.2212 81.2134 20.9606 83.9011 27.3164H65.9531ZM60.0469 27.3164V5.98131C57.4185 6.11523 54.8371 6.4269 52.3144 6.90453C48.2615 14.2068 44.8994 20.9538 42.2089 27.3164H60.0469ZM60.0469 92.0391V65.9531H34.0093C34.3172 74.3536 36.0964 82.8472 39.3887 92.0391H60.0469ZM108.153 97.9453C101.496 106.535 92.4421 113.173 81.9944 116.858C85.4934 110.227 88.4369 103.967 90.8088 97.9453H108.153ZM112.168 92.0391H92.9758C96.0234 83.0451 97.7207 74.5074 98.0106 65.9531H120.019C119.535 75.4455 116.733 84.3258 112.168 92.0391ZM120.019 60.0469H97.9867C97.6203 51.3086 95.7869 42.5348 92.5229 33.2227H111.723C116.551 41.1054 119.52 50.2502 120.019 60.0469ZM107.571 27.3164H90.2923C87.9424 21.4966 85.0648 15.4383 81.668 9.02798C91.9513 12.5842 100.902 18.9966 107.571 27.3164ZM33.587 33.2227C30.323 42.5348 28.4897 51.3086 28.1233 60.0469H5.98131C6.48047 50.2502 9.44907 41.1054 14.2769 33.2227H33.587ZM44.4665 8.98166C41.059 15.4091 38.1732 21.4826 35.8176 27.3164H18.4285C25.127 18.9603 34.1267 12.5284 44.4665 8.98166ZM28.0994 65.9531C28.3892 74.5074 30.0865 83.0451 33.1341 92.0391H13.8313C9.26639 84.3283 6.46449 75.4524 5.98119 65.9531H28.0994ZM0 126L10.1935 97.3714C3.7467 87.4871 0 75.6811 0 63C0 28.2061 28.2061 0 63 0C97.7939 0 126 28.2061 126 63C126 97.7939 97.7939 126 63 126C50.4117 126 38.6858 122.308 28.8459 115.948L0 126Z" fill="#3D3D3D"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

22985
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,109 +18,114 @@
}, },
"dependencies": { "dependencies": {
"@material-ui/core": "^3.9.3", "@material-ui/core": "^3.9.3",
"@material-ui/icons": "^3.0.1", "@material-ui/icons": "^3.0.2",
"@material-ui/styles": "^3.0.0-alpha.10", "@material-ui/styles": "^3.0.0-alpha.10",
"@types/classnames": "^2.2.6", "apollo": "^2.15.0",
"@types/enzyme": "^3.1.15", "apollo-cache-inmemory": "^1.6.2",
"@types/fuzzaldrin": "^2.1.1", "apollo-client": "^2.6.3",
"@types/moment-timezone": "^0.5.9",
"@types/react-dropzone": "^4.2.2",
"@types/react-helmet": "^5.0.7",
"@types/react-infinite-scroller": "^1.2.0",
"@types/react-sortable-tree": "^0.3.6",
"@types/storybook__addon-storyshots": "^3.4.3",
"@types/string-similarity": "^1.2.0",
"@types/url-join": "^0.8.2",
"apollo": "^2.8.1",
"apollo-cache-inmemory": "^1.3.11",
"apollo-client": "^2.4.7",
"apollo-client-preset": "^1.0.6", "apollo-client-preset": "^1.0.6",
"apollo-link": "^1.2.5", "apollo-link": "^1.2.12",
"apollo-link-batch-http": "^1.2.5", "apollo-link-batch-http": "^1.2.12",
"apollo-link-context": "^1.0.10", "apollo-link-context": "^1.0.18",
"apollo-link-error": "^1.1.2", "apollo-link-error": "^1.1.11",
"apollo-upload-client": "^9.1.0", "apollo-upload-client": "^9.1.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"crc-32": "^1.2.0", "crc-32": "^1.2.0",
"downshift": "^1.31.14", "downshift": "^1.31.16",
"draft-js": "^0.10.5", "draft-js": "^0.10.5",
"draftail": "^1.1.0", "draftail": "^1.2.1",
"draftjs-to-html": "^0.8.4", "draftjs-to-html": "^0.8.4",
"fast-array-diff": "^0.2.0", "fast-array-diff": "^0.2.0",
"fuzzaldrin": "^2.1.0", "fuzzaldrin": "^2.1.0",
"graphql": "^14.0.2", "graphql": "^14.4.2",
"graphql-tag": "^2.10.0", "graphql-tag": "^2.10.1",
"i18next": "^11.2.2", "i18next": "^11.10.2",
"i18next-browser-languagedetector": "^2.2.0", "i18next-browser-languagedetector": "^2.2.4",
"i18next-xhr-backend": "^1.5.1", "i18next-xhr-backend": "^1.5.1",
"is-url": "^1.2.4", "is-url": "^1.2.4",
"jss": "^9.8.7",
"keycode": "^2.2.0", "keycode": "^2.2.0",
"lodash": "^4.17.11", "lodash": "^4.17.14",
"moment-timezone": "^0.5.21", "lodash-es": "^4.17.14",
"qs": "^6.5.2", "moment-timezone": "^0.5.26",
"react": "^16.8.3", "qs": "^6.7.0",
"react-apollo": "^2.5.1", "react": "^16.8.6",
"react-dom": "^16.8.3", "react-apollo": "^2.5.8",
"react-dropzone": "^8.0.3", "react-dom": "^16.8.6",
"react-helmet": "^5.2.0", "react-dropzone": "^8.2.0",
"react-helmet": "^5.2.1",
"react-infinite-scroller": "^1.2.4", "react-infinite-scroller": "^1.2.4",
"react-inlinesvg": "^0.8.1", "react-inlinesvg": "^0.8.4",
"react-jss": "^8.4.0", "react-jss": "^8.6.1",
"react-moment": "^0.7.7", "react-moment": "^0.7.9",
"react-router": "^5.0.0", "react-router": "^5.0.1",
"react-router-dom": "^5.0.0", "react-router-dom": "^5.0.1",
"react-router-navigation-prompt": "^1.8.2",
"react-sortable-hoc": "^0.6.8", "react-sortable-hoc": "^0.6.8",
"react-sortable-tree": "^2.6.2", "react-sortable-tree": "^2.6.2",
"react-svg": "^2.2.11", "react-svg": "^2.2.11",
"slugify": "^1.3.4", "slugify": "^1.3.4",
"string-similarity": "^1.2.0", "string-similarity": "^1.2.2",
"typescript": "^3.2.2", "typescript": "^3.5.3",
"universal-cookie": "^2.1.2", "url-join": "^4.0.1",
"url-join": "^4.0.0", "use-react-router": "^1.0.7"
"use-react-router": "^1.0.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.2.0", "@babel/core": "^7.5.4",
"@babel/plugin-proposal-class-properties": "^7.2.1", "@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/plugin-proposal-decorators": "^7.2.0", "@babel/plugin-proposal-decorators": "^7.4.4",
"@babel/plugin-proposal-object-rest-spread": "^7.2.0", "@babel/plugin-proposal-object-rest-spread": "^7.5.4",
"@babel/preset-env": "^7.2.0", "@babel/preset-env": "^7.5.4",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.2.0", "@babel/runtime": "^7.5.4",
"@storybook/addon-storyshots": "^4.1.4", "@storybook/addon-storyshots": "^5.1.9",
"@storybook/react": "^4.1.4", "@storybook/react": "^5.1.9",
"@types/draft-js": "^0.10.27", "@testing-library/react-hooks": "^1.1.0",
"@types/i18next": "^8.4.3", "@types/classnames": "^2.2.9",
"@types/jest": "^23.3.1", "@types/draft-js": "^0.10.34",
"@types/react": "^16.8.6", "@types/enzyme": "^3.10.2",
"@types/react-dom": "^16.8.2", "@types/fuzzaldrin": "^2.1.2",
"@types/react-router-dom": "^4.2.6", "@types/i18next": "^8.4.6",
"@types/react-test-renderer": "^16.8.1", "@types/jest": "^23.3.14",
"@types/storybook__react": "^4.0.0", "@types/lodash-es": "^4.17.3",
"@types/moment-timezone": "^0.5.12",
"@types/react": "16.8.12",
"@types/react-dom": "^16.8.4",
"@types/react-dropzone": "^4.2.2",
"@types/react-helmet": "^5.0.8",
"@types/react-infinite-scroller": "^1.2.1",
"@types/react-router-dom": "^4.3.4",
"@types/react-sortable-hoc": "^0.6.5",
"@types/react-sortable-tree": "^0.3.6",
"@types/react-test-renderer": "^16.8.2",
"@types/storybook__addon-storyshots": "^3.4.9",
"@types/storybook__react": "^4.0.2",
"@types/string-similarity": "^1.2.1",
"@types/url-join": "^0.8.3",
"babel-core": "^7.0.0-bridge.0", "babel-core": "^7.0.0-bridge.0",
"babel-jest": "^23.6.0", "babel-jest": "^23.6.0",
"babel-loader": "^8.0.4", "babel-loader": "^8.0.6",
"enzyme": "^3.7.0", "codecov": "^3.5.0",
"enzyme-adapter-react-16": "^1.7.0", "enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"enzyme-to-json": "^3.3.5", "enzyme-to-json": "^3.3.5",
"file-loader": "^1.1.11", "file-loader": "^1.1.11",
"fork-ts-checker-webpack-plugin": "^0.5.2", "fork-ts-checker-webpack-plugin": "^0.5.2",
"fsevents": "^1.2.9", "fsevents": "^1.2.9",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jest": "^24.7.1", "jest": "^24.8.0",
"jest-file": "^1.0.0", "jest-file": "^1.0.0",
"plop": "^2.2.0", "plop": "^2.4.0",
"react-test-renderer": "^16.8.6", "react-test-renderer": "^16.8.6",
"regenerator-runtime": "^0.11.1", "regenerator-runtime": "^0.11.1",
"testcafe": "^1.3.3",
"ts-jest": "^23.10.5", "ts-jest": "^23.10.5",
"ts-loader": "^5.3.2", "ts-loader": "^5.4.5",
"tsconfig-paths-webpack-plugin": "^3.2.0", "tsconfig-paths-webpack-plugin": "^3.2.0",
"tslint": "^5.11.0", "tslint": "^5.18.0",
"tslint-config-prettier": "^1.15.0", "tslint-config-prettier": "^1.18.0",
"webpack": "^4.27.0", "webpack": "^4.35.3",
"webpack-bundle-tracker": "^0.3.0", "webpack-bundle-tracker": "^0.3.0",
"webpack-cli": "^3.1.2", "webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2" "webpack-dev-server": "^3.7.2"
}, },
"babel": { "babel": {
@ -160,7 +165,8 @@
], ],
"moduleNameMapper": { "moduleNameMapper": {
"@assets(.*)$": "<rootDir>/assets/$1", "@assets(.*)$": "<rootDir>/assets/$1",
"@saleor(.*)$": "<rootDir>/src/$1" "@saleor(.*)$": "<rootDir>/src/$1",
"^lodash-es(.*)$": "lodash/$1"
} }
}, },
"scripts": { "scripts": {
@ -172,6 +178,7 @@
"start": "webpack-dev-server --open", "start": "webpack-dev-server --open",
"storybook": "start-storybook -p 3000 -c src/storybook/", "storybook": "start-storybook -p 3000 -c src/storybook/",
"build-storybook": "build-storybook -c src/storybook/ -o build/storybook", "build-storybook": "build-storybook -c src/storybook/ -o build/storybook",
"test": "jest src/" "test": "jest src/",
"test-e2e": "testcafe 'chrome:headless' .testcafe"
} }
} }

View file

@ -1,6 +1,6 @@
import CssBaseline from "@material-ui/core/CssBaseline"; import CssBaseline from "@material-ui/core/CssBaseline";
import { createStyles, withStyles } from "@material-ui/core/styles"; import { createStyles, withStyles } from "@material-ui/core/styles";
import * as React from "react"; import React from "react";
const styles = createStyles({ const styles = createStyles({
"@global": { "@global": {

View file

@ -1,4 +1,4 @@
import * as React from "react"; import React from "react";
import NotFoundPage from "./components/NotFoundPage"; import NotFoundPage from "./components/NotFoundPage";
import useNavigator from "./hooks/useNavigator"; import useNavigator from "./hooks/useNavigator";

View file

@ -0,0 +1,40 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import i18n from "../../../i18n";
export interface AttributeBulkDeleteDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
quantity: string;
open: boolean;
onConfirm: () => void;
onClose: () => void;
}
const AttributeBulkDeleteDialog: React.StatelessComponent<
AttributeBulkDeleteDialogProps
> = ({ confirmButtonState, quantity, onClose, onConfirm, open }) => (
<ActionDialog
open={open}
confirmButtonState={confirmButtonState}
onClose={onClose}
onConfirm={onConfirm}
title={i18n.t("Remove attributes")}
variant="delete"
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to remove <strong>{{ quantity }}</strong> attributes?",
{
quantity
}
)
}}
/>
</ActionDialog>
);
AttributeBulkDeleteDialog.displayName = "AttributeBulkDeleteDialog";
export default AttributeBulkDeleteDialog;

View file

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

View file

@ -0,0 +1,45 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import i18n from "@saleor/i18n";
export interface AttributeDeleteDialogProps {
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
onConfirm: () => void;
onClose: () => void;
name: string;
}
const AttributeDeleteDialog: React.FC<AttributeDeleteDialogProps> = ({
name,
confirmButtonState,
onClose,
onConfirm,
open
}) => (
<ActionDialog
open={open}
onClose={onClose}
confirmButtonState={confirmButtonState}
onConfirm={onConfirm}
variant="delete"
title={i18n.t("Remove attribute")}
>
<DialogContentText
dangerouslySetInnerHTML={{
__html: i18n.t(
"Are you sure you want to remove <strong>{{ name }}</strong>?",
{
name
}
)
}}
/>
</ActionDialog>
);
AttributeDeleteDialog.displayName = "AttributeDeleteDialog";
export default AttributeDeleteDialog;

View file

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

View file

@ -0,0 +1,98 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import React from "react";
import slugify from "slugify";
import CardTitle from "@saleor/components/CardTitle";
import ControlledSwitch from "@saleor/components/ControlledSwitch";
import FormSpacer from "@saleor/components/FormSpacer";
import SingleSelectField from "@saleor/components/SingleSelectField";
import i18n from "@saleor/i18n";
import { FormErrors } from "@saleor/types";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
import { AttributePageFormData } from "../AttributePage";
export interface AttributeDetailsProps {
canChangeType: boolean;
data: AttributePageFormData;
disabled: boolean;
errors: FormErrors<"name" | "slug" | "inputType">;
onChange: (event: React.ChangeEvent<any>) => void;
}
const inputTypeChoices = [
{
label: i18n.t("Dropdown"),
value: AttributeInputTypeEnum.DROPDOWN
},
{
label: i18n.t("Multiple Select"),
value: AttributeInputTypeEnum.MULTISELECT
}
];
const AttributeDetails: React.FC<AttributeDetailsProps> = ({
canChangeType,
data,
disabled,
errors,
onChange
}) => (
<Card>
<CardTitle title={i18n.t("General Information")} />
<CardContent>
<TextField
disabled={disabled}
error={!!errors.name}
label={i18n.t("Default Label")}
name={"name" as keyof AttributePageFormData}
fullWidth
helperText={errors.name}
value={data.name}
onChange={onChange}
/>
<FormSpacer />
<TextField
disabled={disabled}
error={!!errors.slug}
label={i18n.t("Attribute Code")}
name={"slug" as keyof AttributePageFormData}
placeholder={slugify(data.name).toLowerCase()}
fullWidth
helperText={
errors.slug ||
i18n.t("This is used internally. Make sure you dont use spaces", {
context: "slug input"
})
}
value={data.slug}
onChange={onChange}
/>
<FormSpacer />
<SingleSelectField
choices={inputTypeChoices}
disabled={disabled || !canChangeType}
error={!!errors.inputType}
hint={errors.inputType}
label={i18n.t("Catalog Input type for Store Owner", {
context: "attribute input type"
})}
name="inputType"
onChange={onChange}
value={data.inputType}
/>
<FormSpacer />
<ControlledSwitch
checked={data.valueRequired}
label={i18n.t("Value Required", {
context: "attribute must have value"
})}
name={"valueRequired" as keyof AttributePageFormData}
onChange={onChange}
/>
</CardContent>
</Card>
);
AttributeDetails.displayName = "AttributeDetails";
export default AttributeDetails;

View file

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

View file

@ -0,0 +1,183 @@
import { Theme } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination";
import i18n from "@saleor/i18n";
import { renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "@saleor/types";
import { translateBoolean } from "@saleor/utils/i18n";
import { AttributeList_attributes_edges_node } from "../../types/AttributeList";
export interface AttributeListProps extends ListProps, ListActions {
attributes: AttributeList_attributes_edges_node[];
}
const useStyles = makeStyles((theme: Theme) => ({
[theme.breakpoints.up("lg")]: {
colFaceted: {
width: 150
},
colName: {
width: "auto"
},
colSearchable: {
width: 150
},
colSlug: {
width: 200
},
colVisible: {
width: 150
}
},
colFaceted: {
textAlign: "center"
},
colName: {},
colSearchable: {
textAlign: "center"
},
colSlug: {},
colVisible: {
textAlign: "center"
},
link: {
cursor: "pointer"
}
}));
const numberOfColumns = 6;
const AttributeList: React.StatelessComponent<AttributeListProps> = ({
attributes,
disabled,
isChecked,
onNextPage,
onPreviousPage,
onRowClick,
pageInfo,
selected,
toggle,
toggleAll,
toolbar
}) => {
const classes = useStyles({});
return (
<Table>
<TableHead
colSpan={numberOfColumns}
selected={selected}
disabled={disabled}
items={attributes}
toggleAll={toggleAll}
toolbar={toolbar}
>
<TableCell className={classes.colSlug}>
{i18n.t("Attribute Code", { context: "attribute slug" })}
</TableCell>
<TableCell className={classes.colName}>
{i18n.t("Default Label", { context: "attribute name" })}
</TableCell>
<TableCell className={classes.colVisible}>
{i18n.t("Visible", { context: "attribute visibility" })}
</TableCell>
<TableCell className={classes.colSearchable}>
{i18n.t("Searchable", {
context: "attribute can be searched in dashboard"
})}
</TableCell>
<TableCell className={classes.colFaceted}>
{i18n.t("Use in faceted search", {
context: "attribute can be searched in storefront"
})}
</TableCell>
</TableHead>
<TableFooter>
<TableRow>
<TablePagination
colSpan={numberOfColumns}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage}
hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false
}
onPreviousPage={onPreviousPage}
/>
</TableRow>
</TableFooter>
<TableBody>
{renderCollection(
attributes,
attribute => {
const isSelected = attribute ? isChecked(attribute.id) : false;
return (
<TableRow
selected={isSelected}
hover={!!attribute}
key={attribute ? attribute.id : "skeleton"}
onClick={attribute && onRowClick(attribute.id)}
className={classes.link}
>
<TableCell padding="checkbox">
<Checkbox
checked={isSelected}
disabled={disabled}
disableClickPropagation
onChange={() => toggle(attribute.id)}
/>
</TableCell>
<TableCell className={classes.colSlug}>
{attribute ? attribute.slug : <Skeleton />}
</TableCell>
<TableCell className={classes.colName}>
{attribute ? attribute.name : <Skeleton />}
</TableCell>
<TableCell className={classes.colVisible}>
{attribute ? (
translateBoolean(attribute.visibleInStorefront)
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colSearchable}>
{attribute ? (
translateBoolean(attribute.filterableInDashboard)
) : (
<Skeleton />
)}
</TableCell>
<TableCell className={classes.colFaceted}>
{attribute ? (
translateBoolean(attribute.filterableInStorefront)
) : (
<Skeleton />
)}
</TableCell>
</TableRow>
);
},
() => (
<TableRow>
<TableCell colSpan={numberOfColumns}>
{i18n.t("No attributes found")}
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
);
};
AttributeList.displayName = "AttributeList";
export default AttributeList;

View file

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

View file

@ -0,0 +1,33 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import AddIcon from "@material-ui/icons/Add";
import React from "react";
import Container from "../../../components/Container";
import PageHeader from "../../../components/PageHeader";
import i18n from "../../../i18n";
import { ListActions, PageListProps } from "../../../types";
import { AttributeList_attributes_edges_node } from "../../types/AttributeList";
import AttributeList from "../AttributeList/AttributeList";
export interface AttributeListPageProps extends PageListProps, ListActions {
attributes: AttributeList_attributes_edges_node[];
}
const AttributeListPage: React.FC<AttributeListPageProps> = ({
onAdd,
...listProps
}) => (
<Container>
<PageHeader title={i18n.t("Attributes")}>
<Button onClick={onAdd} color="primary" variant="contained">
{i18n.t("Add attribute")} <AddIcon />
</Button>
</PageHeader>
<Card>
<AttributeList {...listProps} />
</Card>
</Container>
);
AttributeListPage.displayName = "AttributeListPage";
export default AttributeListPage;

View file

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

View file

@ -0,0 +1,160 @@
import React from "react";
import slugify from "slugify";
import AppHeader from "@saleor/components/AppHeader";
import CardSpacer from "@saleor/components/CardSpacer";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import Container from "@saleor/components/Container";
import Form from "@saleor/components/Form";
import Grid from "@saleor/components/Grid";
import PageHeader from "@saleor/components/PageHeader";
import SaveButtonBar from "@saleor/components/SaveButtonBar";
import i18n from "@saleor/i18n";
import { maybe } from "@saleor/misc";
import { ReorderAction, UserError } from "@saleor/types";
import { AttributeInputTypeEnum } from "@saleor/types/globalTypes";
import {
AttributeDetailsFragment,
AttributeDetailsFragment_values
} from "../../types/AttributeDetailsFragment";
import AttributeDetails from "../AttributeDetails";
import AttributeProperties from "../AttributeProperties";
import AttributeValues from "../AttributeValues";
export interface AttributePageProps {
attribute: AttributeDetailsFragment | null;
disabled: boolean;
errors: UserError[];
saveButtonBarState: ConfirmButtonTransitionState;
values: AttributeDetailsFragment_values[];
onBack: () => void;
onDelete: () => void;
onSubmit: (data: AttributePageFormData) => void;
onValueAdd: () => void;
onValueDelete: (id: string) => void;
onValueReorder: ReorderAction;
onValueUpdate: (id: string) => void;
}
export interface AttributePageFormData {
filterableInDashboard: boolean;
inputType: AttributeInputTypeEnum;
filterableInStorefront: boolean;
name: string;
slug: string;
storefrontSearchPosition: string;
valueRequired: boolean;
visibleInStorefront: boolean;
}
const AttributePage: React.FC<AttributePageProps> = ({
attribute,
disabled,
errors,
saveButtonBarState,
values,
onBack,
onDelete,
onSubmit,
onValueAdd,
onValueDelete,
onValueReorder,
onValueUpdate
}) => {
const initialForm: AttributePageFormData =
attribute === null
? {
filterableInDashboard: true,
filterableInStorefront: true,
inputType: AttributeInputTypeEnum.DROPDOWN,
name: "",
slug: "",
storefrontSearchPosition: "",
valueRequired: true,
visibleInStorefront: true
}
: {
filterableInDashboard: maybe(
() => attribute.filterableInDashboard,
true
),
filterableInStorefront: maybe(
() => attribute.filterableInStorefront,
true
),
inputType: maybe(
() => attribute.inputType,
AttributeInputTypeEnum.DROPDOWN
),
name: maybe(() => attribute.name, ""),
slug: maybe(() => attribute.slug, ""),
storefrontSearchPosition: maybe(
() => attribute.storefrontSearchPosition.toString(),
""
),
valueRequired: maybe(() => attribute.valueRequired, true),
visibleInStorefront: maybe(() => attribute.visibleInStorefront, true)
};
const handleSubmit = (data: AttributePageFormData) =>
onSubmit({
...data,
slug: data.slug || slugify(data.name).toLowerCase()
});
return (
<Form errors={errors} initial={initialForm} onSubmit={handleSubmit}>
{({ change, errors: formErrors, data, submit }) => (
<Container>
<AppHeader onBack={onBack}>{i18n.t("Attributes")}</AppHeader>
<PageHeader
title={
attribute === null
? i18n.t("Create New Attribute", {
context: "page title"
})
: maybe(() => attribute.name)
}
/>
<Grid>
<div>
<AttributeDetails
canChangeType={attribute === null}
data={data}
disabled={disabled}
errors={formErrors}
onChange={change}
/>
<CardSpacer />
<AttributeValues
disabled={disabled}
values={values}
onValueAdd={onValueAdd}
onValueDelete={onValueDelete}
onValueReorder={onValueReorder}
onValueUpdate={onValueUpdate}
/>
</div>
<div>
<AttributeProperties
data={data}
errors={formErrors}
disabled={disabled}
onChange={change}
/>
</div>
</Grid>
<SaveButtonBar
disabled={disabled}
state={saveButtonBarState}
onCancel={onBack}
onSave={submit}
onDelete={attribute === null ? undefined : onDelete}
/>
</Container>
)}
</Form>
);
};
AttributePage.displayName = "AttributePage";
export default AttributePage;

View file

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

View file

@ -0,0 +1,112 @@
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 React from "react";
import CardSpacer from "@saleor/components/CardSpacer";
import CardTitle from "@saleor/components/CardTitle";
import ControlledSwitch from "@saleor/components/ControlledSwitch";
import FormSpacer from "@saleor/components/FormSpacer";
import Hr from "@saleor/components/Hr";
import i18n from "@saleor/i18n";
import { FormErrors } from "@saleor/types";
import { AttributePageFormData } from "../AttributePage";
export interface AttributePropertiesProps {
data: AttributePageFormData;
disabled: boolean;
errors: FormErrors<"storefrontSearchPosition">;
onChange: (event: React.ChangeEvent<any>) => void;
}
const AttributeProperties: React.FC<AttributePropertiesProps> = ({
data,
errors,
disabled,
onChange
}) => (
<Card>
<CardTitle title={i18n.t("Properties")} />
<CardContent>
{/* <Typography variant="subtitle1">
{i18n.t("General Properties")}
</Typography>
<Hr />
<CardSpacer />
<ControlledSwitch
name={"" as keyof AttributePageFormData}
checked={false}
disabled={disabled}
label={
<>
<span>{i18n.t("Variant Attribute")}</span>
<Typography variant="caption">
{i18n.t(
"If enabled, you'll be able to use this attribute to create product variants"
)}
</Typography>
</>
}
onChange={onChange}
/> */}
<Typography variant="subtitle1">
{i18n.t("Storefront Properties")}
</Typography>
<Hr />
<ControlledSwitch
name={"filterableInStorefront" as keyof AttributePageFormData}
checked={data.filterableInStorefront}
disabled={disabled}
label={i18n.t("Use in faceted navigation")}
onChange={onChange}
/>
{data.filterableInStorefront && (
<TextField
disabled={disabled}
error={!!errors.storefrontSearchPosition}
fullWidth
helperText={errors.storefrontSearchPosition}
name={"storefrontSearchPosition" as keyof AttributePageFormData}
label={i18n.t("Position in faceted navigation")}
value={data.storefrontSearchPosition}
onChange={onChange}
/>
)}
<FormSpacer />
<ControlledSwitch
name={"visibleInStorefront" as keyof AttributePageFormData}
checked={data.visibleInStorefront}
disabled={disabled}
label={i18n.t("Visible on Product Page in Storefront", {
context: "attribute"
})}
onChange={onChange}
/>
<CardSpacer />
<Typography variant="subtitle1">
{i18n.t("Dashboard Properties")}
</Typography>
<Hr />
<CardSpacer />
<ControlledSwitch
name={"filterableInDashboard" as keyof AttributePageFormData}
checked={data.filterableInDashboard}
disabled={disabled}
label={i18n.t("Use in Filtering")}
secondLabel={
<Typography variant="caption">
{i18n.t(
"If enabled, youll be able to use this attribute to filter products in product list."
)}
</Typography>
}
onChange={onChange}
/>
</CardContent>
</Card>
);
AttributeProperties.displayName = "AttributeProperties";
export default AttributeProperties;

View file

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

View file

@ -0,0 +1,52 @@
import DialogContentText from "@material-ui/core/DialogContentText";
import React from "react";
import ActionDialog from "@saleor/components/ActionDialog";
import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton";
import i18n from "@saleor/i18n";
export interface AttributeValueDeleteDialogProps {
attributeName: string;
confirmButtonState: ConfirmButtonTransitionState;
open: boolean;
name: string;
useName?: boolean;
onConfirm: () => void;
onClose: () => void;
}
const AttributeValueDeleteDialog: React.FC<AttributeValueDeleteDialogProps> = ({
attributeName,
name,
confirmButtonState,
useName,
onClose,
onConfirm,
open
}) => (
<ActionDialog
open={open}
onClose={onClose}
confirmButtonState={confirmButtonState}
onConfirm={onConfirm}
variant="delete"
title={i18n.t("Remove attribute value")}
>
<DialogContentText>
{useName
? i18n.t(
'Are you sure you want to remove "{{ name }}" value? If you remove it you wont be able to assign it to any of the products with "{{ attributeName }}" attribute.',
{
attributeName,
name
}
)
: i18n.t('Are you sure you want to remove "{{ name }}" value?', {
name
})}
</DialogContentText>
</ActionDialog>
);
AttributeValueDeleteDialog.displayName = "AttributeValueDeleteDialog";
export default AttributeValueDeleteDialog;

View file

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

View file

@ -0,0 +1,97 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import TextField from "@material-ui/core/TextField";
import React from "react";
import ConfirmButton, {
ConfirmButtonTransitionState
} from "@saleor/components/ConfirmButton";
import Form from "@saleor/components/Form";
import useModalDialogErrors from "@saleor/hooks/useModalDialogErrors";
import i18n from "@saleor/i18n";
import { maybe } from "@saleor/misc";
import { UserError } from "@saleor/types";
import { AttributeDetails_attribute_values } from "../../types/AttributeDetails";
export interface AttributeValueEditDialogFormData {
name: string;
}
export interface AttributeValueEditDialogProps {
attributeValue: AttributeDetails_attribute_values | null;
confirmButtonState: ConfirmButtonTransitionState;
disabled: boolean;
errors: UserError[];
open: boolean;
onSubmit: (data: AttributeValueEditDialogFormData) => void;
onClose: () => void;
}
const AttributeValueEditDialog: React.StatelessComponent<
AttributeValueEditDialogProps
> = ({
attributeValue,
confirmButtonState,
disabled,
errors: apiErrors,
onClose,
onSubmit,
open
}) => {
const initialForm: AttributeValueEditDialogFormData = {
name: maybe(() => attributeValue.name, "")
};
const errors = useModalDialogErrors(apiErrors, open);
return (
<Dialog onClose={onClose} open={open} fullWidth maxWidth="sm">
<DialogTitle>
{attributeValue === null
? i18n.t("Add Value", {
context: "add attribute value"
})
: i18n.t("Edit Value", {
context: "edit attribute value"
})}
</DialogTitle>
<Form errors={errors} initial={initialForm} onSubmit={onSubmit}>
{({ change, data, errors: formErrors, submit }) => (
<>
<DialogContent>
<TextField
autoFocus
disabled={disabled}
error={!!formErrors.name}
fullWidth
helperText={formErrors.name}
name={"name" as keyof AttributeValueEditDialogFormData}
label={i18n.t("Name", {
context: "attribute name"
})}
value={data.name}
onChange={change}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
{i18n.t("Cancel", { context: "button" })}
</Button>
<ConfirmButton
transitionState={confirmButtonState}
color="primary"
variant="contained"
onClick={submit}
>
{i18n.t("Save")}
</ConfirmButton>
</DialogActions>
</>
)}
</Form>
</Dialog>
);
};
AttributeValueEditDialog.displayName = "AttributeValueEditDialog";
export default AttributeValueEditDialog;

View file

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

View file

@ -0,0 +1,129 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import IconButton from "@material-ui/core/IconButton";
import { Theme } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import DeleteIcon from "@material-ui/icons/Delete";
import makeStyles from "@material-ui/styles/makeStyles";
import React from "react";
import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton";
import {
SortableTableBody,
SortableTableRow
} from "@saleor/components/SortableTable";
import i18n from "@saleor/i18n";
import { maybe, renderCollection, stopPropagation } from "@saleor/misc";
import { ReorderAction } from "@saleor/types";
import { AttributeDetailsFragment_values } from "../../types/AttributeDetailsFragment";
export interface AttributeValuesProps {
disabled: boolean;
values: AttributeDetailsFragment_values[];
onValueAdd: () => void;
onValueDelete: (id: string) => void;
onValueReorder: ReorderAction;
onValueUpdate: (id: string) => void;
}
const useStyles = makeStyles((theme: Theme) => ({
columnAdmin: {
width: "50%"
},
columnDrag: {
width: 48 + theme.spacing.unit * 1.5
},
columnStore: {
width: "50%"
},
dragIcon: {
cursor: "grab"
},
iconCell: {
"&:last-child": {
paddingRight: theme.spacing.unit
},
width: 48 + theme.spacing.unit * 1.5
},
link: {
cursor: "pointer"
}
}));
const AttributeValues: React.FC<AttributeValuesProps> = ({
disabled,
onValueAdd,
onValueDelete,
onValueReorder,
onValueUpdate,
values
}) => {
const classes = useStyles({});
return (
<Card>
<CardTitle
title={i18n.t("Attribute Values")}
toolbar={
<Button color="primary" variant="text" onClick={onValueAdd}>
{i18n.t("Add value", { context: "button" })}
</Button>
}
/>
<Table>
<TableHead>
<TableRow>
<TableCell className={classes.columnDrag} />
<TableCell className={classes.columnAdmin}>
{i18n.t("Admin")}
</TableCell>
<TableCell className={classes.columnStore}>
{i18n.t("Default Store View")}
</TableCell>
<TableCell />
</TableRow>
</TableHead>
<SortableTableBody onSortEnd={onValueReorder}>
{renderCollection(
values,
(value, valueIndex) => (
<SortableTableRow
className={!!value ? classes.link : undefined}
hover={!!value}
onClick={!!value ? () => onValueUpdate(value.id) : undefined}
key={maybe(() => value.id)}
index={valueIndex || 0}
>
<TableCell className={classes.columnAdmin}>
{maybe(() => value.slug) ? value.slug : <Skeleton />}
</TableCell>
<TableCell className={classes.columnStore}>
{maybe(() => value.name) ? value.name : <Skeleton />}
</TableCell>
<TableCell className={classes.iconCell}>
<IconButton
disabled={disabled}
onClick={stopPropagation(() => onValueDelete(value.id))}
>
<DeleteIcon color="primary" />
</IconButton>
</TableCell>
</SortableTableRow>
),
() => (
<TableRow>
<TableCell colSpan={2}>{i18n.t("No values found")}</TableCell>
</TableRow>
)
)}
</SortableTableBody>
</Table>
</Card>
);
};
AttributeValues.displayName = "AttributeValues";
export default AttributeValues;

View file

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

512
src/attributes/fixtures.ts Normal file
View file

@ -0,0 +1,512 @@
import {
AttributeInputTypeEnum,
AttributeValueType
} from "@saleor/types/globalTypes";
import { AttributeList_attributes_edges_node } from "./types/AttributeList";
export const attribute = {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo5",
inputType: AttributeInputTypeEnum.DROPDOWN,
name: "Author",
slug: "author",
storefrontSearchPosition: 2,
valueRequired: true,
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI0",
name: "John Doe",
slug: "john-doe",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI1",
name: "Milionare Pirate",
slug: "milionare-pirate",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
};
export const attributes: AttributeList_attributes_edges_node[] = [
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: false,
id: "UHJvZHVjdEF0dHJpYnV0ZTo5",
name: "Author",
slug: "author",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI0",
name: "John Doe",
slug: "john-doe",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI1",
name: "Milionare Pirate",
slug: "milionare-pirate",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: false,
id: "UHJvZHVjdEF0dHJpYnV0ZTo2",
name: "Box Size",
slug: "box-size",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE1",
name: "100g",
slug: "100g",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE2",
name: "250g",
slug: "250g",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE3",
name: "500g",
slug: "500g",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE4",
name: "1kg",
slug: "1kg",
sortOrder: 3,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: false
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZToz",
name: "Brand",
slug: "brand",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjY=",
name: "Saleor",
slug: "saleor",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: false
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo4",
name: "Candy Box Size",
slug: "candy-box-size",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIx",
name: "100g",
slug: "100g",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIy",
name: "250g",
slug: "250g",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIz",
name: "500g",
slug: "500g",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: false
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo1",
name: "Coffee Genre",
slug: "coffee-genre",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEz",
name: "Arabica",
slug: "arabica",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE0",
name: "Robusta",
slug: "robusta",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZToy",
name: "Collar",
slug: "collar",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM=",
name: "Round",
slug: "round",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjQ=",
name: "V-Neck",
slug: "v-neck",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjU=",
name: "Polo",
slug: "polo",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: false,
id: "UHJvZHVjdEF0dHJpYnV0ZTox",
name: "Color",
slug: "color",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE=",
name: "Blue",
slug: "blue",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI=",
name: "White",
slug: "white",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: false,
id: "UHJvZHVjdEF0dHJpYnV0ZToxMg==",
name: "Cover",
slug: "cover",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMw",
name: "Soft",
slug: "soft",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMx",
name: "Hard",
slug: "hard",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMy",
name: "Middle soft",
slug: "middle-soft",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjMz",
name: "Middle hard",
slug: "middle-hard",
sortOrder: 3,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM0",
name: "Middle",
slug: "middle",
sortOrder: 4,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjM1",
name: "Very hard",
slug: "very-hard",
sortOrder: 5,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: false
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo3",
name: "Flavor",
slug: "flavor",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjE5",
name: "Sour",
slug: "sour",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjIw",
name: "Sweet",
slug: "sweet",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: false,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZToxMQ==",
name: "Language",
slug: "language",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI4",
name: "English",
slug: "english",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI5",
name: "Pirate",
slug: "pirate",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZToxMA==",
name: "Publisher",
slug: "publisher",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI2",
name: "Mirumee Press",
slug: "mirumee-press",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjI3",
name: "Saleor Publishing",
slug: "saleor-publishing",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
},
{
node: {
__typename: "Attribute" as "Attribute",
filterableInDashboard: true,
filterableInStorefront: true,
id: "UHJvZHVjdEF0dHJpYnV0ZTo0",
name: "Size",
slug: "size",
values: [
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjc=",
name: "XS",
slug: "xs",
sortOrder: 0,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjg=",
name: "S",
slug: "s",
sortOrder: 1,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjk=",
name: "M",
slug: "m",
sortOrder: 2,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEw",
name: "L",
slug: "l",
sortOrder: 3,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEx",
name: "XL",
slug: "xl",
sortOrder: 4,
type: AttributeValueType.STRING,
value: ""
},
{
__typename: "AttributeValue" as "AttributeValue",
id: "UHJvZHVjdEF0dHJpYnV0ZVZhbHVlOjEy",
name: "XXL",
slug: "xxl",
sortOrder: 5,
type: AttributeValueType.STRING,
value: ""
}
],
visibleInStorefront: true
}
}
].map(edge => edge.node);

55
src/attributes/index.tsx Normal file
View file

@ -0,0 +1,55 @@
import { parse as parseQs } from "qs";
import React from "react";
import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { WindowTitle } from "../components/WindowTitle";
import i18n from "../i18n";
import {
attributeAddPath,
AttributeAddUrlQueryParams,
attributeListPath,
AttributeListUrlQueryParams,
attributePath,
AttributeUrlQueryParams
} from "./urls";
import AttributeCreateComponent from "./views/AttributeCreate";
import AttributeDetailsComponent from "./views/AttributeDetails";
import AttributeListComponent from "./views/AttributeList";
const AttributeList: React.FC<RouteComponentProps<{}>> = ({ location }) => {
const qs = parseQs(location.search.substr(1));
const params: AttributeListUrlQueryParams = qs;
return <AttributeListComponent params={params} />;
};
const AttributeCreate: React.FC<RouteComponentProps<{}>> = ({ location }) => {
const qs = parseQs(location.search.substr(1));
const params: AttributeAddUrlQueryParams = qs;
return <AttributeCreateComponent params={params} />;
};
const AttributeDetails: React.FC<RouteComponentProps<{ id: string }>> = ({
location,
match
}) => {
const qs = parseQs(location.search.substr(1));
const params: AttributeUrlQueryParams = qs;
return (
<AttributeDetailsComponent
id={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
export const AttributeSection: React.FC = () => (
<>
<WindowTitle title={i18n.t("Attributes")} />
<Switch>
<Route exact path={attributeListPath} component={AttributeList} />
<Route exact path={attributeAddPath} component={AttributeCreate} />
<Route path={attributePath(":id")} component={AttributeDetails} />
</Switch>
</>
);
export default AttributeSection;

182
src/attributes/mutations.ts Normal file
View file

@ -0,0 +1,182 @@
import gql from "graphql-tag";
import { TypedMutation } from "@saleor/mutations";
import { attributeDetailsFragment } from "./queries";
import {
AttributeBulkDelete,
AttributeBulkDeleteVariables
} from "./types/AttributeBulkDelete";
import {
AttributeCreate,
AttributeCreateVariables
} from "./types/AttributeCreate";
import {
AttributeDelete,
AttributeDeleteVariables
} from "./types/AttributeDelete";
import {
AttributeUpdate,
AttributeUpdateVariables
} from "./types/AttributeUpdate";
import {
AttributeValueCreate,
AttributeValueCreateVariables
} from "./types/AttributeValueCreate";
import {
AttributeValueDelete,
AttributeValueDeleteVariables
} from "./types/AttributeValueDelete";
import {
AttributeValueReorder,
AttributeValueReorderVariables
} from "./types/AttributeValueReorder";
import {
AttributeValueUpdate,
AttributeValueUpdateVariables
} from "./types/AttributeValueUpdate";
const attributeBulkDelete = gql`
mutation AttributeBulkDelete($ids: [ID!]!) {
attributeBulkDelete(ids: $ids) {
errors {
field
message
}
}
}
`;
export const AttributeBulkDeleteMutation = TypedMutation<
AttributeBulkDelete,
AttributeBulkDeleteVariables
>(attributeBulkDelete);
const attributeDelete = gql`
mutation AttributeDelete($id: ID!) {
attributeDelete(id: $id) {
errors {
field
message
}
}
}
`;
export const AttributeDeleteMutation = TypedMutation<
AttributeDelete,
AttributeDeleteVariables
>(attributeDelete);
export const attributeUpdateMutation = gql`
${attributeDetailsFragment}
mutation AttributeUpdate($id: ID!, $input: AttributeUpdateInput!) {
attributeUpdate(id: $id, input: $input) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeUpdateMutation = TypedMutation<
AttributeUpdate,
AttributeUpdateVariables
>(attributeUpdateMutation);
const attributeValueDelete = gql`
${attributeDetailsFragment}
mutation AttributeValueDelete($id: ID!) {
attributeValueDelete(id: $id) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeValueDeleteMutation = TypedMutation<
AttributeValueDelete,
AttributeValueDeleteVariables
>(attributeValueDelete);
export const attributeValueUpdateMutation = gql`
${attributeDetailsFragment}
mutation AttributeValueUpdate($id: ID!, $input: AttributeValueCreateInput!) {
attributeValueUpdate(id: $id, input: $input) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeValueUpdateMutation = TypedMutation<
AttributeValueUpdate,
AttributeValueUpdateVariables
>(attributeValueUpdateMutation);
export const attributeValueCreateMutation = gql`
${attributeDetailsFragment}
mutation AttributeValueCreate($id: ID!, $input: AttributeValueCreateInput!) {
attributeValueCreate(attribute: $id, input: $input) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeValueCreateMutation = TypedMutation<
AttributeValueCreate,
AttributeValueCreateVariables
>(attributeValueCreateMutation);
export const attributeCreateMutation = gql`
${attributeDetailsFragment}
mutation AttributeCreate($input: AttributeCreateInput!) {
attributeCreate(input: $input) {
errors {
field
message
}
attribute {
...AttributeDetailsFragment
}
}
}
`;
export const AttributeCreateMutation = TypedMutation<
AttributeCreate,
AttributeCreateVariables
>(attributeCreateMutation);
const attributeValueReorderMutation = gql`
mutation AttributeValueReorder($id: ID!, $move: ReorderInput!) {
attributeReorderValues(attributeId: $id, moves: [$move]) {
errors {
field
message
}
attribute {
id
values {
id
}
}
}
}
`;
export const AttributeValueReorderMutation = TypedMutation<
AttributeValueReorder,
AttributeValueReorderVariables
>(attributeValueReorderMutation);

86
src/attributes/queries.ts Normal file
View file

@ -0,0 +1,86 @@
import gql from "graphql-tag";
import { pageInfoFragment, TypedQuery } from "../queries";
import {
AttributeDetails,
AttributeDetailsVariables
} from "./types/AttributeDetails";
import { AttributeList, AttributeListVariables } from "./types/AttributeList";
export const attributeFragment = gql`
fragment AttributeFragment on Attribute {
id
name
slug
visibleInStorefront
filterableInDashboard
filterableInStorefront
}
`;
export const attributeDetailsFragment = gql`
${attributeFragment}
fragment AttributeDetailsFragment on Attribute {
...AttributeFragment
inputType
storefrontSearchPosition
valueRequired
values {
id
name
slug
type
value
}
}
`;
const attributeDetails = gql`
${attributeDetailsFragment}
query AttributeDetails($id: ID!) {
attribute(id: $id) {
...AttributeDetailsFragment
}
}
`;
export const AttributeDetailsQuery = TypedQuery<
AttributeDetails,
AttributeDetailsVariables
>(attributeDetails);
const attributeList = gql`
${attributeFragment}
${pageInfoFragment}
query AttributeList(
$query: String
$inCategory: ID
$inCollection: ID
$before: String
$after: String
$first: Int
$last: Int
) {
attributes(
query: $query
inCategory: $inCategory
inCollection: $inCollection
before: $before
after: $after
first: $first
last: $last
) {
edges {
node {
...AttributeFragment
}
}
pageInfo {
...PageInfoFragment
}
}
}
`;
export const AttributeListQuery = TypedQuery<
AttributeList,
AttributeListVariables
>(attributeList);

View file

@ -0,0 +1,26 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: AttributeBulkDelete
// ====================================================
export interface AttributeBulkDelete_attributeBulkDelete_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeBulkDelete_attributeBulkDelete {
__typename: "AttributeBulkDelete";
errors: AttributeBulkDelete_attributeBulkDelete_errors[] | null;
}
export interface AttributeBulkDelete {
attributeBulkDelete: AttributeBulkDelete_attributeBulkDelete | null;
}
export interface AttributeBulkDeleteVariables {
ids: string[];
}

View file

@ -0,0 +1,52 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeCreateInput, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeCreate
// ====================================================
export interface AttributeCreate_attributeCreate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeCreate_attributeCreate_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeCreate_attributeCreate_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeCreate_attributeCreate_attribute_values | null)[] | null;
}
export interface AttributeCreate_attributeCreate {
__typename: "AttributeCreate";
errors: AttributeCreate_attributeCreate_errors[] | null;
attribute: AttributeCreate_attributeCreate_attribute | null;
}
export interface AttributeCreate {
attributeCreate: AttributeCreate_attributeCreate | null;
}
export interface AttributeCreateVariables {
input: AttributeCreateInput;
}

View file

@ -0,0 +1,26 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: AttributeDelete
// ====================================================
export interface AttributeDelete_attributeDelete_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeDelete_attributeDelete {
__typename: "AttributeDelete";
errors: AttributeDelete_attributeDelete_errors[] | null;
}
export interface AttributeDelete {
attributeDelete: AttributeDelete_attributeDelete | null;
}
export interface AttributeDeleteVariables {
id: string;
}

View file

@ -0,0 +1,40 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL query operation: AttributeDetails
// ====================================================
export interface AttributeDetails_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeDetails_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeDetails_attribute_values | null)[] | null;
}
export interface AttributeDetails {
attribute: AttributeDetails_attribute | null;
}
export interface AttributeDetailsVariables {
id: string;
}

View file

@ -0,0 +1,32 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL fragment: AttributeDetailsFragment
// ====================================================
export interface AttributeDetailsFragment_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeDetailsFragment {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeDetailsFragment_values | null)[] | null;
}

View file

@ -0,0 +1,17 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: AttributeFragment
// ====================================================
export interface AttributeFragment {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}

View file

@ -0,0 +1,50 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: AttributeList
// ====================================================
export interface AttributeList_attributes_edges_node {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
}
export interface AttributeList_attributes_edges {
__typename: "AttributeCountableEdge";
node: AttributeList_attributes_edges_node;
}
export interface AttributeList_attributes_pageInfo {
__typename: "PageInfo";
endCursor: string | null;
hasNextPage: boolean;
hasPreviousPage: boolean;
startCursor: string | null;
}
export interface AttributeList_attributes {
__typename: "AttributeCountableConnection";
edges: AttributeList_attributes_edges[];
pageInfo: AttributeList_attributes_pageInfo;
}
export interface AttributeList {
attributes: AttributeList_attributes | null;
}
export interface AttributeListVariables {
query?: string | null;
inCategory?: string | null;
inCollection?: string | null;
before?: string | null;
after?: string | null;
first?: number | null;
last?: number | null;
}

View file

@ -0,0 +1,53 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeUpdateInput, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeUpdate
// ====================================================
export interface AttributeUpdate_attributeUpdate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeUpdate_attributeUpdate_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeUpdate_attributeUpdate_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeUpdate_attributeUpdate_attribute_values | null)[] | null;
}
export interface AttributeUpdate_attributeUpdate {
__typename: "AttributeUpdate";
errors: AttributeUpdate_attributeUpdate_errors[] | null;
attribute: AttributeUpdate_attributeUpdate_attribute | null;
}
export interface AttributeUpdate {
attributeUpdate: AttributeUpdate_attributeUpdate | null;
}
export interface AttributeUpdateVariables {
id: string;
input: AttributeUpdateInput;
}

View file

@ -0,0 +1,53 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueCreate
// ====================================================
export interface AttributeValueCreate_attributeValueCreate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeValueCreate_attributeValueCreate_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeValueCreate_attributeValueCreate_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeValueCreate_attributeValueCreate_attribute_values | null)[] | null;
}
export interface AttributeValueCreate_attributeValueCreate {
__typename: "AttributeValueCreate";
errors: AttributeValueCreate_attributeValueCreate_errors[] | null;
attribute: AttributeValueCreate_attributeValueCreate_attribute | null;
}
export interface AttributeValueCreate {
attributeValueCreate: AttributeValueCreate_attributeValueCreate | null;
}
export interface AttributeValueCreateVariables {
id: string;
input: AttributeValueCreateInput;
}

View file

@ -0,0 +1,52 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueDelete
// ====================================================
export interface AttributeValueDelete_attributeValueDelete_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeValueDelete_attributeValueDelete_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeValueDelete_attributeValueDelete_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeValueDelete_attributeValueDelete_attribute_values | null)[] | null;
}
export interface AttributeValueDelete_attributeValueDelete {
__typename: "AttributeValueDelete";
errors: AttributeValueDelete_attributeValueDelete_errors[] | null;
attribute: AttributeValueDelete_attributeValueDelete_attribute | null;
}
export interface AttributeValueDelete {
attributeValueDelete: AttributeValueDelete_attributeValueDelete | null;
}
export interface AttributeValueDeleteVariables {
id: string;
}

View file

@ -0,0 +1,41 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { ReorderInput } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueReorder
// ====================================================
export interface AttributeValueReorder_attributeReorderValues_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeValueReorder_attributeReorderValues_attribute_values {
__typename: "AttributeValue";
id: string;
}
export interface AttributeValueReorder_attributeReorderValues_attribute {
__typename: "Attribute";
id: string;
values: (AttributeValueReorder_attributeReorderValues_attribute_values | null)[] | null;
}
export interface AttributeValueReorder_attributeReorderValues {
__typename: "AttributeReorderValues";
errors: AttributeValueReorder_attributeReorderValues_errors[] | null;
attribute: AttributeValueReorder_attributeReorderValues_attribute | null;
}
export interface AttributeValueReorder {
attributeReorderValues: AttributeValueReorder_attributeReorderValues | null;
}
export interface AttributeValueReorderVariables {
id: string;
move: ReorderInput;
}

View file

@ -0,0 +1,53 @@
/* tslint:disable */
/* eslint-disable */
// This file was automatically generated and should not be edited.
import { AttributeValueCreateInput, AttributeInputTypeEnum, AttributeValueType } from "./../../types/globalTypes";
// ====================================================
// GraphQL mutation operation: AttributeValueUpdate
// ====================================================
export interface AttributeValueUpdate_attributeValueUpdate_errors {
__typename: "Error";
field: string | null;
message: string | null;
}
export interface AttributeValueUpdate_attributeValueUpdate_attribute_values {
__typename: "AttributeValue";
id: string;
name: string | null;
slug: string | null;
type: AttributeValueType | null;
value: string | null;
}
export interface AttributeValueUpdate_attributeValueUpdate_attribute {
__typename: "Attribute";
id: string;
name: string | null;
slug: string | null;
visibleInStorefront: boolean;
filterableInDashboard: boolean;
filterableInStorefront: boolean;
inputType: AttributeInputTypeEnum | null;
storefrontSearchPosition: number;
valueRequired: boolean;
values: (AttributeValueUpdate_attributeValueUpdate_attribute_values | null)[] | null;
}
export interface AttributeValueUpdate_attributeValueUpdate {
__typename: "AttributeValueUpdate";
errors: AttributeValueUpdate_attributeValueUpdate_errors[] | null;
attribute: AttributeValueUpdate_attributeValueUpdate_attribute | null;
}
export interface AttributeValueUpdate {
attributeValueUpdate: AttributeValueUpdate_attributeValueUpdate | null;
}
export interface AttributeValueUpdateVariables {
id: string;
input: AttributeValueCreateInput;
}

38
src/attributes/urls.ts Normal file
View file

@ -0,0 +1,38 @@
import { stringify as stringifyQs } from "qs";
import urlJoin from "url-join";
import { BulkAction, Dialog, Pagination, SingleAction } from "../types";
export const attributeSection = "/attributes/";
export type AttributeListUrlDialog = "remove";
export type AttributeListUrlQueryParams = BulkAction &
Dialog<AttributeListUrlDialog> &
Pagination;
export const attributeListPath = attributeSection;
export const attributeListUrl = (params?: AttributeListUrlQueryParams) =>
attributeListPath + "?" + stringifyQs(params);
export type AttributeAddUrlDialog =
| "add-value"
| "edit-value"
| "remove-value"
| "remove-values";
export type AttributeAddUrlQueryParams = Dialog<AttributeAddUrlDialog> &
SingleAction;
export const attributeAddPath = urlJoin(attributeSection, "add");
export const attributeAddUrl = (params?: AttributeAddUrlQueryParams) =>
attributeAddPath + "?" + stringifyQs(params);
export type AttributeUrlDialog =
| "add-value"
| "edit-value"
| "remove"
| "remove-value"
| "remove-values";
export type AttributeUrlQueryParams = BulkAction &
Dialog<AttributeUrlDialog> &
SingleAction;
export const attributePath = (id: string) => urlJoin(attributeSection, id);
export const attributeUrl = (id: string, params?: AttributeUrlQueryParams) =>
attributePath(encodeURIComponent(id)) + "?" + stringifyQs(params);

View file

@ -0,0 +1,206 @@
import React from "react";
import slugify from "slugify";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import i18n from "@saleor/i18n";
import { getMutationState, maybe } from "@saleor/misc";
import { ReorderEvent, UserError } from "@saleor/types";
import {
add,
isSelected,
move,
remove,
updateAtIndex
} from "@saleor/utils/lists";
import AttributePage from "../../components/AttributePage";
import AttributeValueDeleteDialog from "../../components/AttributeValueDeleteDialog";
import AttributeValueEditDialog, {
AttributeValueEditDialogFormData
} from "../../components/AttributeValueEditDialog";
import { AttributeCreateMutation } from "../../mutations";
import { AttributeCreate } from "../../types/AttributeCreate";
import {
attributeAddUrl,
AttributeAddUrlDialog,
AttributeAddUrlQueryParams,
attributeListUrl,
attributeUrl
} from "../../urls";
interface AttributeDetailsProps {
params: AttributeAddUrlQueryParams;
}
function areValuesEqual(
a: AttributeValueEditDialogFormData,
b: AttributeValueEditDialogFormData
) {
return a.name === b.name;
}
const AttributeDetails: React.FC<AttributeDetailsProps> = ({ params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const [values, setValues] = React.useState<
AttributeValueEditDialogFormData[]
>([]);
const [valueErrors, setValueErrors] = React.useState<UserError[]>([]);
const id = params.id ? parseInt(params.id, 0) : undefined;
const closeModal = () =>
navigate(
attributeAddUrl({
...params,
action: undefined,
id: undefined
}),
true
);
const openModal = (action: AttributeAddUrlDialog, valueId?: string) =>
navigate(
attributeAddUrl({
...params,
action,
id: valueId
})
);
const handleValueDelete = () => {
setValues(remove(values[params.id], values, areValuesEqual));
closeModal();
};
const handleCreate = (data: AttributeCreate) => {
if (data.attributeCreate.errors.length === 0) {
notify({ text: i18n.t("Successfully created attribute") });
navigate(attributeUrl(data.attributeCreate.attribute.id));
}
};
const handleValueUpdate = (input: AttributeValueEditDialogFormData) => {
if (isSelected(input, values, areValuesEqual)) {
setValueErrors([
{
field: "name",
message: i18n.t("A value named {{ name }} already exists", {
context: "value edit error",
name: input.name
})
}
]);
} else {
setValues(updateAtIndex(input, values, id));
closeModal();
}
};
const handleValueCreate = (input: AttributeValueEditDialogFormData) => {
if (isSelected(input, values, areValuesEqual)) {
setValueErrors([
{
field: "name",
message: i18n.t("A value named {{ name }} already exists", {
context: "value edit error",
name: input.name
})
}
]);
} else {
setValues(add(input, values));
closeModal();
}
};
const handleValueReorder = ({ newIndex, oldIndex }: ReorderEvent) =>
setValues(move(values[oldIndex], values, areValuesEqual, newIndex));
return (
<AttributeCreateMutation onCompleted={handleCreate}>
{(attributeCreate, attributeCreateOpts) => {
const createTransitionState = getMutationState(
attributeCreateOpts.called,
attributeCreateOpts.loading,
maybe(() => attributeCreateOpts.data.attributeCreate.errors)
);
return (
<>
<AttributePage
attribute={null}
disabled={false}
errors={maybe(
() => attributeCreateOpts.data.attributeCreate.errors,
[]
)}
onBack={() => navigate(attributeListUrl())}
onDelete={undefined}
onSubmit={input =>
attributeCreate({
variables: {
input: {
...input,
storefrontSearchPosition: parseInt(
input.storefrontSearchPosition,
0
),
values: values.map(value => ({
name: value.name
}))
}
}
})
}
onValueAdd={() => openModal("add-value")}
onValueDelete={id => openModal("remove-value", id)}
onValueReorder={handleValueReorder}
onValueUpdate={id => openModal("edit-value", id)}
saveButtonBarState={createTransitionState}
values={values.map((value, valueIndex) => ({
__typename: "AttributeValue" as "AttributeValue",
id: valueIndex.toString(),
slug: slugify(value.name).toLowerCase(),
sortOrder: valueIndex,
type: null,
value: null,
...value
}))}
/>
<AttributeValueEditDialog
attributeValue={null}
confirmButtonState="default"
disabled={false}
errors={valueErrors}
open={params.action === "add-value"}
onClose={closeModal}
onSubmit={handleValueCreate}
/>
{values.length > 0 && (
<>
<AttributeValueDeleteDialog
attributeName={undefined}
open={params.action === "remove-value"}
name={maybe(() => values[id].name, "...")}
confirmButtonState="default"
onClose={closeModal}
onConfirm={handleValueDelete}
/>
<AttributeValueEditDialog
attributeValue={maybe(() => values[params.id])}
confirmButtonState="default"
disabled={false}
errors={valueErrors}
open={params.action === "edit-value"}
onClose={closeModal}
onSubmit={handleValueUpdate}
/>
</>
)}
</>
);
}}
</AttributeCreateMutation>
);
};
AttributeDetails.displayName = "AttributeDetails";
export default AttributeDetails;

View file

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

View file

@ -0,0 +1,360 @@
import React from "react";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import i18n from "@saleor/i18n";
import { getMutationState, maybe } from "@saleor/misc";
import { ReorderEvent } from "@saleor/types";
import { move } from "@saleor/utils/lists";
import AttributeDeleteDialog from "../../components/AttributeDeleteDialog";
import AttributePage from "../../components/AttributePage";
import AttributeValueDeleteDialog from "../../components/AttributeValueDeleteDialog";
import AttributeValueEditDialog from "../../components/AttributeValueEditDialog";
import {
AttributeDeleteMutation,
AttributeUpdateMutation,
AttributeValueCreateMutation,
AttributeValueDeleteMutation,
AttributeValueReorderMutation,
AttributeValueUpdateMutation
} from "../../mutations";
import { AttributeDetailsQuery } from "../../queries";
import { AttributeDelete } from "../../types/AttributeDelete";
import { AttributeUpdate } from "../../types/AttributeUpdate";
import { AttributeValueCreate } from "../../types/AttributeValueCreate";
import { AttributeValueDelete } from "../../types/AttributeValueDelete";
import { AttributeValueReorder } from "../../types/AttributeValueReorder";
import { AttributeValueUpdate } from "../../types/AttributeValueUpdate";
import {
attributeListUrl,
attributeUrl,
AttributeUrlDialog,
AttributeUrlQueryParams
} from "../../urls";
interface AttributeDetailsProps {
id: string;
params: AttributeUrlQueryParams;
}
const AttributeDetails: React.FC<AttributeDetailsProps> = ({ id, params }) => {
const navigate = useNavigator();
const notify = useNotifier();
const closeModal = () =>
navigate(
attributeUrl(id, {
...params,
action: undefined,
id: undefined,
ids: undefined
}),
true
);
const openModal = (action: AttributeUrlDialog, valueId?: string) =>
navigate(
attributeUrl(id, {
...params,
action,
id: valueId
})
);
const handleDelete = (data: AttributeDelete) => {
if (data.attributeDelete.errors.length === 0) {
notify({ text: i18n.t("Attribute removed") });
navigate(attributeListUrl());
}
};
const handleValueDelete = (data: AttributeValueDelete) => {
if (data.attributeValueDelete.errors.length === 0) {
notify({ text: i18n.t("Value removed") });
closeModal();
}
};
const handleUpdate = (data: AttributeUpdate) => {
if (data.attributeUpdate.errors.length === 0) {
notify({ text: i18n.t("Saved changes") });
}
};
const handleValueUpdate = (data: AttributeValueUpdate) => {
if (data.attributeValueUpdate.errors.length === 0) {
notify({ text: i18n.t("Saved changes") });
closeModal();
}
};
const handleValueCreate = (data: AttributeValueCreate) => {
if (data.attributeValueCreate.errors.length === 0) {
notify({ text: i18n.t("Added new value") });
closeModal();
}
};
const handleValueReorderMutation = (data: AttributeValueReorder) => {
if (data.attributeReorderValues.errors.length !== 0) {
notify({
text: i18n.t("Error: {{ errorMessage }}", {
errorMessage: data.attributeReorderValues.errors[0].message
})
});
}
};
return (
<AttributeDetailsQuery variables={{ id }}>
{({ data, loading }) => (
<AttributeDeleteMutation onCompleted={handleDelete}>
{(attributeDelete, attributeDeleteOpts) => (
<AttributeValueDeleteMutation onCompleted={handleValueDelete}>
{(attributeValueDelete, attributeValueDeleteOpts) => (
<AttributeUpdateMutation onCompleted={handleUpdate}>
{(attributeUpdate, attributeUpdateOpts) => (
<AttributeValueUpdateMutation
onCompleted={handleValueUpdate}
>
{(attributeValueUpdate, attributeValueUpdateOpts) => (
<AttributeValueCreateMutation
onCompleted={handleValueCreate}
>
{(attributeValueCreate, attributeValueCreateOpts) => (
<AttributeValueReorderMutation
onCompleted={handleValueReorderMutation}
>
{attributeValueReorder => {
const deleteTransitionState = getMutationState(
attributeDeleteOpts.called,
attributeDeleteOpts.loading,
maybe(
() =>
attributeDeleteOpts.data.attributeDelete
.errors
)
);
const deleteValueTransitionState = getMutationState(
attributeValueDeleteOpts.called,
attributeValueDeleteOpts.loading,
maybe(
() =>
attributeValueDeleteOpts.data
.attributeValueDelete.errors
)
);
const updateTransitionState = getMutationState(
attributeUpdateOpts.called,
attributeUpdateOpts.loading,
maybe(
() =>
attributeUpdateOpts.data.attributeUpdate
.errors
)
);
const updateValueTransitionState = getMutationState(
attributeValueUpdateOpts.called,
attributeValueUpdateOpts.loading,
maybe(
() =>
attributeValueUpdateOpts.data
.attributeValueUpdate.errors
)
);
const createValueTransitionState = getMutationState(
attributeValueCreateOpts.called,
attributeValueCreateOpts.loading,
maybe(
() =>
attributeValueCreateOpts.data
.attributeValueCreate.errors
)
);
const handleValueReorder = ({
newIndex,
oldIndex
}: ReorderEvent) =>
attributeValueReorder({
optimisticResponse: {
attributeReorderValues: {
__typename: "AttributeReorderValues",
attribute: {
...data.attribute,
values: move(
data.attribute.values[oldIndex],
data.attribute.values,
(a, b) => a.id === b.id,
newIndex
)
},
errors: []
}
},
variables: {
id,
move: {
id: data.attribute.values[oldIndex].id,
sortOrder: newIndex - oldIndex
}
}
});
return (
<>
<AttributePage
attribute={maybe(() => data.attribute)}
disabled={loading}
errors={maybe(
() =>
attributeUpdateOpts.data
.attributeUpdate.errors,
[]
)}
onBack={() =>
navigate(attributeListUrl())
}
onDelete={() => openModal("remove")}
onSubmit={data => {
const input = {
...data,
inputType: undefined
};
attributeUpdate({
variables: {
id,
input: {
...input,
storefrontSearchPosition: parseInt(
input.storefrontSearchPosition,
0
)
}
}
});
}}
onValueAdd={() => openModal("add-value")}
onValueDelete={id =>
openModal("remove-value", id)
}
onValueReorder={handleValueReorder}
onValueUpdate={id =>
openModal("edit-value", id)
}
saveButtonBarState={updateTransitionState}
values={maybe(
() => data.attribute.values
)}
/>
<AttributeDeleteDialog
open={params.action === "remove"}
name={maybe(
() => data.attribute.name,
"..."
)}
confirmButtonState={deleteTransitionState}
onClose={closeModal}
onConfirm={() =>
attributeDelete({
variables: {
id
}
})
}
/>
<AttributeValueDeleteDialog
attributeName={maybe(
() => data.attribute.name,
"..."
)}
open={params.action === "remove-value"}
name={maybe(
() =>
data.attribute.values.find(
value => params.id === value.id
).name,
"..."
)}
useName={true}
confirmButtonState={
deleteValueTransitionState
}
onClose={closeModal}
onConfirm={() =>
attributeValueDelete({
variables: {
id: params.id
}
})
}
/>
<AttributeValueEditDialog
attributeValue={null}
confirmButtonState={
createValueTransitionState
}
disabled={loading}
errors={maybe(
() =>
attributeValueCreateOpts.data
.attributeValueCreate.errors,
[]
)}
open={params.action === "add-value"}
onClose={closeModal}
onSubmit={input =>
attributeValueCreate({
variables: {
id,
input
}
})
}
/>
<AttributeValueEditDialog
attributeValue={maybe(() =>
data.attribute.values.find(
value => params.id === value.id
)
)}
confirmButtonState={
updateValueTransitionState
}
disabled={loading}
errors={maybe(
() =>
attributeValueUpdateOpts.data
.attributeValueUpdate.errors,
[]
)}
open={params.action === "edit-value"}
onClose={closeModal}
onSubmit={input =>
attributeValueUpdate({
variables: {
id: data.attribute.values.find(
value => params.id === value.id
).id,
input
}
})
}
/>
</>
);
}}
</AttributeValueReorderMutation>
)}
</AttributeValueCreateMutation>
)}
</AttributeValueUpdateMutation>
)}
</AttributeUpdateMutation>
)}
</AttributeValueDeleteMutation>
)}
</AttributeDeleteMutation>
)}
</AttributeDetailsQuery>
);
};
AttributeDetails.displayName = "AttributeDetails";
export default AttributeDetails;

View file

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

View file

@ -0,0 +1,137 @@
import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete";
import React from "react";
import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, {
createPaginationState
} from "@saleor/hooks/usePaginator";
import { PAGINATE_BY } from "../../../config";
import useBulkActions from "../../../hooks/useBulkActions";
import i18n from "../../../i18n";
import { getMutationState, maybe } from "../../../misc";
import AttributeBulkDeleteDialog from "../../components/AttributeBulkDeleteDialog";
import AttributeListPage from "../../components/AttributeListPage";
import { AttributeBulkDeleteMutation } from "../../mutations";
import { AttributeListQuery } from "../../queries";
import { AttributeBulkDelete } from "../../types/AttributeBulkDelete";
import {
attributeAddUrl,
attributeListUrl,
AttributeListUrlDialog,
AttributeListUrlQueryParams,
attributeUrl
} from "../../urls";
interface AttributeListProps {
params: AttributeListUrlQueryParams;
}
const AttributeList: React.FC<AttributeListProps> = ({ params }) => {
const navigate = useNavigator();
const paginate = usePaginator();
const notify = useNotifier();
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids
);
const closeModal = () =>
navigate(
attributeListUrl({
...params,
action: undefined,
ids: undefined
}),
true
);
const openModal = (action: AttributeListUrlDialog, ids?: string[]) =>
navigate(
attributeListUrl({
...params,
action,
ids
})
);
const paginationState = createPaginationState(PAGINATE_BY, params);
const queryVariables = React.useMemo(() => paginationState, [params]);
return (
<AttributeListQuery variables={queryVariables}>
{({ data, loading, refetch }) => {
const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
maybe(() => data.attributes.pageInfo),
paginationState,
params
);
const handleBulkDelete = (data: AttributeBulkDelete) => {
if (data.attributeBulkDelete.errors.length === 0) {
closeModal();
notify({
text: i18n.t("Attributes removed")
});
reset();
refetch();
}
};
return (
<AttributeBulkDeleteMutation onCompleted={handleBulkDelete}>
{(attributeBulkDelete, attributeBulkDeleteOpts) => {
const bulkDeleteMutationState = getMutationState(
attributeBulkDeleteOpts.called,
attributeBulkDeleteOpts.loading,
maybe(
() => attributeBulkDeleteOpts.data.attributeBulkDelete.errors
)
);
return (
<>
<AttributeListPage
attributes={maybe(() =>
data.attributes.edges.map(edge => edge.node)
)}
disabled={loading || attributeBulkDeleteOpts.loading}
isChecked={isSelected}
onAdd={() => navigate(attributeAddUrl())}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
onRowClick={id => () => navigate(attributeUrl(id))}
pageInfo={pageInfo}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
toolbar={
<IconButton
color="primary"
onClick={() => openModal("remove", listElements)}
>
<DeleteIcon />
</IconButton>
}
/>
<AttributeBulkDeleteDialog
confirmButtonState={bulkDeleteMutationState}
open={params.action === "remove"}
onConfirm={() =>
attributeBulkDelete({ variables: { ids: params.ids } })
}
onClose={closeModal}
quantity={maybe(() => params.ids.length.toString(), "...")}
/>
</>
);
}}
</AttributeBulkDeleteMutation>
);
}}
</AttributeListQuery>
);
};
AttributeList.displayName = "AttributeList";
export default AttributeList;

View file

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

View file

@ -1,4 +1,4 @@
import * as React from "react"; import React from "react";
import { getMutationProviderData } from "../misc"; import { getMutationProviderData } from "../misc";
import { PartialMutationProviderOutput } from "../types"; import { PartialMutationProviderOutput } from "../types";

View file

@ -1,6 +1,6 @@
import CircularProgress from "@material-ui/core/CircularProgress"; import CircularProgress from "@material-ui/core/CircularProgress";
import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles"; import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles";
import * as React from "react"; import React from "react";
const styles = createStyles({ const styles = createStyles({
root: { root: {

View file

@ -7,17 +7,17 @@ import {
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import * as React from "react"; import React from "react";
import SVG from "react-inlinesvg"; import SVG from "react-inlinesvg";
import * as backgroundArt from "@assets/images/login-background.svg"; import backgroundArt from "@assets/images/login-background.svg";
import * as saleorDarkLogo from "@assets/images/logo-dark.svg"; import saleorDarkLogo from "@assets/images/logo-dark.svg";
import * as saleorLightLogo from "@assets/images/logo-light.svg"; import saleorLightLogo from "@assets/images/logo-light.svg";
import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox"; import { ControlledCheckbox } from "@saleor/components/ControlledCheckbox";
import Form from "@saleor/components/Form"; import Form from "@saleor/components/Form";
import { FormSpacer } from "@saleor/components/FormSpacer"; import { FormSpacer } from "@saleor/components/FormSpacer";
import useTheme from "@saleor/hooks/useTheme"; import useTheme from "@saleor/hooks/useTheme";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
export interface FormData { export interface FormData {
email: string; email: string;

View file

@ -1,4 +1,4 @@
import * as React from "react"; import React from "react";
import { Route, RouteProps } from "react-router-dom"; import { Route, RouteProps } from "react-router-dom";
import AppLayout from "@saleor/components/AppLayout"; import AppLayout from "@saleor/components/AppLayout";

View file

@ -1,4 +1,4 @@
import * as React from "react"; import React from "react";
import { User } from "./types/User"; import { User } from "./types/User";

View file

@ -16,6 +16,9 @@ export const fragmentUser = gql`
code code
name name
} }
avatar {
url
}
} }
`; `;

View file

@ -20,6 +20,11 @@ export interface TokenAuth_tokenCreate_user_permissions {
name: string; name: string;
} }
export interface TokenAuth_tokenCreate_user_avatar {
__typename: "Image";
url: string;
}
export interface TokenAuth_tokenCreate_user { export interface TokenAuth_tokenCreate_user {
__typename: "User"; __typename: "User";
id: string; id: string;
@ -29,6 +34,7 @@ export interface TokenAuth_tokenCreate_user {
isStaff: boolean; isStaff: boolean;
note: string | null; note: string | null;
permissions: (TokenAuth_tokenCreate_user_permissions | null)[] | null; permissions: (TokenAuth_tokenCreate_user_permissions | null)[] | null;
avatar: TokenAuth_tokenCreate_user_avatar | null;
} }
export interface TokenAuth_tokenCreate { export interface TokenAuth_tokenCreate {

View file

@ -14,6 +14,11 @@ export interface User_permissions {
name: string; name: string;
} }
export interface User_avatar {
__typename: "Image";
url: string;
}
export interface User { export interface User {
__typename: "User"; __typename: "User";
id: string; id: string;
@ -23,4 +28,5 @@ export interface User {
isStaff: boolean; isStaff: boolean;
note: string | null; note: string | null;
permissions: (User_permissions | null)[] | null; permissions: (User_permissions | null)[] | null;
avatar: User_avatar | null;
} }

View file

@ -14,6 +14,11 @@ export interface VerifyToken_tokenVerify_user_permissions {
name: string; name: string;
} }
export interface VerifyToken_tokenVerify_user_avatar {
__typename: "Image";
url: string;
}
export interface VerifyToken_tokenVerify_user { export interface VerifyToken_tokenVerify_user {
__typename: "User"; __typename: "User";
id: string; id: string;
@ -23,6 +28,7 @@ export interface VerifyToken_tokenVerify_user {
isStaff: boolean; isStaff: boolean;
note: string | null; note: string | null;
permissions: (VerifyToken_tokenVerify_user_permissions | null)[] | null; permissions: (VerifyToken_tokenVerify_user_permissions | null)[] | null;
avatar: VerifyToken_tokenVerify_user_avatar | null;
} }
export interface VerifyToken_tokenVerify { export interface VerifyToken_tokenVerify {

View file

@ -1,4 +1,4 @@
import * as React from "react"; import React from "react";
import LoginPage, { FormData } from "../components/LoginPage"; import LoginPage, { FormData } from "../components/LoginPage";
import { UserContext } from "../index"; import { UserContext } from "../index";

View file

@ -5,7 +5,7 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import * as React from "react"; import React from "react";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";

View file

@ -1,5 +1,5 @@
import { ContentState, convertToRaw, RawDraftContentState } from "draft-js"; import { ContentState, convertToRaw, RawDraftContentState } from "draft-js";
import * as React from "react"; import React from "react";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import { CardSpacer } from "@saleor/components/CardSpacer"; import { CardSpacer } from "@saleor/components/CardSpacer";

View file

@ -10,7 +10,7 @@ import {
withStyles, withStyles,
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import * as React from "react"; import React from "react";
import i18n from "../../../i18n"; import i18n from "../../../i18n";

View file

@ -3,7 +3,7 @@ import CardContent from "@material-ui/core/CardContent";
import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles"; import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import * as React from "react"; import React from "react";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";

View file

@ -11,16 +11,16 @@ import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter"; import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import * as React from "react"; import React from "react";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox"; import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { renderCollection } from "../../../misc"; import { renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "@saleor/types";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -67,11 +67,14 @@ interface CategoryListProps
onAdd?(); onAdd?();
} }
const numberOfColumns = 4;
const CategoryList = withStyles(styles, { name: "CategoryList" })( const CategoryList = withStyles(styles, { name: "CategoryList" })(
({ ({
categories, categories,
classes, classes,
disabled, disabled,
settings,
isRoot, isRoot,
pageInfo, pageInfo,
isChecked, isChecked,
@ -82,6 +85,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
onAdd, onAdd,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick onRowClick
}: CategoryListProps) => ( }: CategoryListProps) => (
<Card> <Card>
@ -97,6 +101,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
)} )}
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={categories} items={categories}
@ -116,9 +121,11 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={4} colSpan={numberOfColumns}
settings={settings}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={ hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false pageInfo && !disabled ? pageInfo.hasPreviousPage : false
} }
@ -144,6 +151,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(category.id)} onChange={() => toggle(category.id)}
/> />
</TableCell> </TableCell>
@ -173,7 +181,7 @@ const CategoryList = withStyles(styles, { name: "CategoryList" })(
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={4}> <TableCell colSpan={numberOfColumns}>
{isRoot {isRoot
? i18n.t("No categories found") ? i18n.t("No categories found")
: i18n.t("No subcategories found")} : i18n.t("No subcategories found")}

View file

@ -1,11 +1,11 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import AddIcon from "@material-ui/icons/Add"; import AddIcon from "@material-ui/icons/Add";
import * as React from "react"; import React from "react";
import Container from "@saleor/components/Container"; import Container from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "../../../types"; import { ListActions, PageListProps } from "@saleor/types";
import CategoryList from "../CategoryList"; import CategoryList from "../CategoryList";
export interface CategoryTableProps extends PageListProps, ListActions { export interface CategoryTableProps extends PageListProps, ListActions {
@ -24,9 +24,11 @@ export interface CategoryTableProps extends PageListProps, ListActions {
export const CategoryListPage: React.StatelessComponent<CategoryTableProps> = ({ export const CategoryListPage: React.StatelessComponent<CategoryTableProps> = ({
categories, categories,
disabled, disabled,
settings,
onAdd, onAdd,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick, onRowClick,
pageInfo, pageInfo,
isChecked, isChecked,
@ -46,9 +48,11 @@ export const CategoryListPage: React.StatelessComponent<CategoryTableProps> = ({
onAdd={onAdd} onAdd={onAdd}
onRowClick={onRowClick} onRowClick={onRowClick}
disabled={disabled} disabled={disabled}
settings={settings}
isRoot={true} isRoot={true}
onNextPage={onNextPage} onNextPage={onNextPage}
onPreviousPage={onPreviousPage} onPreviousPage={onPreviousPage}
onUpdateListSettings={onUpdateListSettings}
pageInfo={pageInfo} pageInfo={pageInfo}
isChecked={isChecked} isChecked={isChecked}
selected={selected} selected={selected}

View file

@ -12,7 +12,7 @@ import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter"; import TableFooter from "@material-ui/core/TableFooter";
import TableHead from "@material-ui/core/TableHead"; import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import * as React from "react"; import React from "react";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";

View file

@ -1,6 +1,6 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import * as React from "react"; import React from "react";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import ProductList from "@saleor/components/ProductList"; import ProductList from "@saleor/components/ProductList";
@ -40,6 +40,10 @@ export const CategoryProductsCard: React.StatelessComponent<
} }
/> />
<ProductList <ProductList
settings={{
columns: ["isPublished", "price", "productType"],
rowNumber: undefined
}}
products={products} products={products}
disabled={disabled} disabled={disabled}
pageInfo={pageInfo} pageInfo={pageInfo}

View file

@ -1,5 +1,5 @@
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import * as React from "react"; import React from "react";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import { CardSpacer } from "@saleor/components/CardSpacer"; import { CardSpacer } from "@saleor/components/CardSpacer";

View file

@ -1,5 +1,5 @@
import { parse as parseQs } from "qs"; import { parse as parseQs } from "qs";
import * as React from "react"; import React from "react";
import { Route, RouteComponentProps, Switch } from "react-router-dom"; import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { WindowTitle } from "../components/WindowTitle"; import { WindowTitle } from "../components/WindowTitle";
import i18n from "../i18n"; import i18n from "../i18n";

View file

@ -1,5 +1,5 @@
import { stringify as stringifyQs } from "qs"; import { stringify as stringifyQs } from "qs";
import * as urlJoin from "url-join"; import urlJoin from "url-join";
import { ActiveTab, BulkAction, Dialog, Pagination } from "../types"; import { ActiveTab, BulkAction, Dialog, Pagination } from "../types";
import { CategoryPageTab } from "./components/CategoryUpdatePage"; import { CategoryPageTab } from "./components/CategoryUpdatePage";

View file

@ -1,4 +1,4 @@
import * as React from "react"; import React from "react";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";

View file

@ -1,7 +1,7 @@
import DialogContentText from "@material-ui/core/DialogContentText"; import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import * as React from "react"; import React from "react";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";

View file

@ -1,17 +1,18 @@
import DialogContentText from "@material-ui/core/DialogContentText"; import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import * as React from "react"; import React from "react";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import usePaginator, { import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import { PAGINATE_BY } from "../../config"; import i18n from "@saleor/i18n";
import i18n from "../../i18n"; import { getMutationState, maybe } from "@saleor/misc";
import { getMutationState, maybe } from "../../misc"; import { ListViews } from "@saleor/types";
import { CategoryListPage } from "../components/CategoryListPage/CategoryListPage"; import { CategoryListPage } from "../components/CategoryListPage/CategoryListPage";
import { TypedCategoryBulkDeleteMutation } from "../mutations"; import { TypedCategoryBulkDeleteMutation } from "../mutations";
import { TypedRootCategoriesQuery } from "../queries"; import { TypedRootCategoriesQuery } from "../queries";
@ -35,8 +36,10 @@ export const CategoryList: React.StatelessComponent<CategoryListProps> = ({
const { isSelected, listElements, toggle, toggleAll, reset } = useBulkActions( const { isSelected, listElements, toggle, toggleAll, reset } = useBulkActions(
params.ids params.ids
); );
const { updateListSettings, settings } = useListSettings(
const paginationState = createPaginationState(PAGINATE_BY, params); ListViews.CATEGORY_LIST
);
const paginationState = createPaginationState(settings.rowNumber, params);
return ( return (
<TypedRootCategoriesQuery displayLoader variables={paginationState}> <TypedRootCategoriesQuery displayLoader variables={paginationState}>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
@ -73,11 +76,13 @@ export const CategoryList: React.StatelessComponent<CategoryListProps> = ({
() => data.categories.edges.map(edge => edge.node), () => data.categories.edges.map(edge => edge.node),
[] []
)} )}
settings={settings}
onAdd={() => navigate(categoryAddUrl())} onAdd={() => navigate(categoryAddUrl())}
onRowClick={id => () => navigate(categoryUrl(id))} onRowClick={id => () => navigate(categoryUrl(id))}
disabled={loading} disabled={loading}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
pageInfo={pageInfo} pageInfo={pageInfo}
isChecked={isSelected} isChecked={isSelected}
selected={listElements.length} selected={listElements.length}

View file

@ -1,7 +1,7 @@
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent"; import CardContent from "@material-ui/core/CardContent";
import { ContentState, convertToRaw, RawDraftContentState } from "draft-js"; import { ContentState, convertToRaw, RawDraftContentState } from "draft-js";
import * as React from "react"; import React from "react";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import { CardSpacer } from "@saleor/components/CardSpacer"; import { CardSpacer } from "@saleor/components/CardSpacer";

View file

@ -3,7 +3,7 @@ import CardContent from "@material-ui/core/CardContent";
import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles"; import { createStyles, withStyles, WithStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import * as React from "react"; import React from "react";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import FormSpacer from "@saleor/components/FormSpacer"; import FormSpacer from "@saleor/components/FormSpacer";

View file

@ -1,5 +1,5 @@
import { RawDraftContentState } from "draft-js"; import { RawDraftContentState } from "draft-js";
import * as React from "react"; import React from "react";
import AppHeader from "@saleor/components/AppHeader"; import AppHeader from "@saleor/components/AppHeader";
import { CardSpacer } from "@saleor/components/CardSpacer"; import { CardSpacer } from "@saleor/components/CardSpacer";

View file

@ -5,7 +5,7 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import * as React from "react"; import React from "react";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card"; import Card from "@material-ui/core/Card";

View file

@ -10,16 +10,16 @@ import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell"; import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter"; import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import * as React from "react"; import React from "react";
import Checkbox from "@saleor/components/Checkbox"; import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel"; import StatusLabel from "@saleor/components/StatusLabel";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { maybe, renderCollection } from "../../../misc"; import { maybe, renderCollection } from "@saleor/misc";
import { ListActions, ListProps } from "../../../types"; import { ListActions, ListProps } from "@saleor/types";
import { CollectionList_collections_edges_node } from "../../types/CollectionList"; import { CollectionList_collections_edges_node } from "../../types/CollectionList";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@ -50,13 +50,17 @@ interface CollectionListProps
collections: CollectionList_collections_edges_node[]; collections: CollectionList_collections_edges_node[];
} }
const numberOfColumns = 5;
const CollectionList = withStyles(styles, { name: "CollectionList" })( const CollectionList = withStyles(styles, { name: "CollectionList" })(
({ ({
classes, classes,
collections, collections,
disabled, disabled,
settings,
onNextPage, onNextPage,
onPreviousPage, onPreviousPage,
onUpdateListSettings,
onRowClick, onRowClick,
pageInfo, pageInfo,
isChecked, isChecked,
@ -68,6 +72,7 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
<Card> <Card>
<Table> <Table>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={collections} items={collections}
@ -89,9 +94,11 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={5} colSpan={numberOfColumns}
settings={settings}
hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false} hasNextPage={pageInfo && !disabled ? pageInfo.hasNextPage : false}
onNextPage={onNextPage} onNextPage={onNextPage}
onUpdateListSettings={onUpdateListSettings}
hasPreviousPage={ hasPreviousPage={
pageInfo && !disabled ? pageInfo.hasPreviousPage : false pageInfo && !disabled ? pageInfo.hasPreviousPage : false
} }
@ -116,6 +123,7 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(collection.id)} onChange={() => toggle(collection.id)}
/> />
</TableCell> </TableCell>
@ -151,7 +159,7 @@ const CollectionList = withStyles(styles, { name: "CollectionList" })(
}, },
() => ( () => (
<TableRow> <TableRow>
<TableCell colSpan={3}> <TableCell colSpan={numberOfColumns}>
{i18n.t("No collections found")} {i18n.t("No collections found")}
</TableCell> </TableCell>
</TableRow> </TableRow>

View file

@ -1,11 +1,11 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import AddIcon from "@material-ui/icons/Add"; import AddIcon from "@material-ui/icons/Add";
import * as React from "react"; import React from "react";
import { Container } from "@saleor/components/Container"; import { Container } from "@saleor/components/Container";
import PageHeader from "@saleor/components/PageHeader"; import PageHeader from "@saleor/components/PageHeader";
import i18n from "../../../i18n"; import i18n from "@saleor/i18n";
import { ListActions, PageListProps } from "../../../types"; import { ListActions, PageListProps } from "@saleor/types";
import { CollectionList_collections_edges_node } from "../../types/CollectionList"; import { CollectionList_collections_edges_node } from "../../types/CollectionList";
import CollectionList from "../CollectionList/CollectionList"; import CollectionList from "../CollectionList/CollectionList";

View file

@ -13,13 +13,15 @@ import TableCell from "@material-ui/core/TableCell";
import TableFooter from "@material-ui/core/TableFooter"; import TableFooter from "@material-ui/core/TableFooter";
import TableRow from "@material-ui/core/TableRow"; import TableRow from "@material-ui/core/TableRow";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import * as React from "react"; import React from "react";
import CardTitle from "@saleor/components/CardTitle"; import CardTitle from "@saleor/components/CardTitle";
import Checkbox from "@saleor/components/Checkbox"; import Checkbox from "@saleor/components/Checkbox";
import Skeleton from "@saleor/components/Skeleton"; import Skeleton from "@saleor/components/Skeleton";
import StatusLabel from "@saleor/components/StatusLabel"; import StatusLabel from "@saleor/components/StatusLabel";
import TableCellAvatar from "@saleor/components/TableCellAvatar"; import TableCellAvatar, {
AVATAR_MARGIN
} from "@saleor/components/TableCellAvatar";
import TableHead from "@saleor/components/TableHead"; import TableHead from "@saleor/components/TableHead";
import TablePagination from "@saleor/components/TablePagination"; import TablePagination from "@saleor/components/TablePagination";
import i18n from "../../../i18n"; import i18n from "../../../i18n";
@ -29,12 +31,27 @@ import { CollectionDetails_collection } from "../../types/CollectionDetails";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
iconCell: { colActions: {
"&:last-child": { "&:last-child": {
paddingRight: 0 paddingRight: 0
}, },
width: 48 + theme.spacing.unit / 2 width: 48 + theme.spacing.unit / 2
}, },
colName: {
width: "auto"
},
colNameLabel: {
marginLeft: AVATAR_MARGIN
},
colPublished: {
width: 200
},
colType: {
width: 200
},
table: {
tableLayout: "fixed"
},
tableRow: { tableRow: {
cursor: "pointer" cursor: "pointer"
} }
@ -48,6 +65,8 @@ export interface CollectionProductsProps
onProductUnassign: (id: string, event: React.MouseEvent<any>) => void; onProductUnassign: (id: string, event: React.MouseEvent<any>) => void;
} }
const numberOfColumns = 5;
const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })( const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
({ ({
classes, classes,
@ -89,24 +108,32 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
</Button> </Button>
} }
/> />
<Table> <Table className={classes.table}>
<TableHead <TableHead
colSpan={numberOfColumns}
selected={selected} selected={selected}
disabled={disabled} disabled={disabled}
items={maybe(() => collection.products.edges.map(edge => edge.node))} items={maybe(() => collection.products.edges.map(edge => edge.node))}
toggleAll={toggleAll} toggleAll={toggleAll}
toolbar={toolbar} toolbar={toolbar}
> >
<TableCell>{i18n.t("Name", { context: "table header" })}</TableCell> <TableCell className={classes.colName}>
<TableCell>{i18n.t("Type", { context: "table header" })}</TableCell> <span className={classes.colNameLabel}>
<TableCell> {i18n.t("Name", { context: "table header" })}
</span>
</TableCell>
<TableCell className={classes.colType}>
{i18n.t("Type", { context: "table header" })}
</TableCell>
<TableCell className={classes.colPublished}>
{i18n.t("Published", { context: "table header" })} {i18n.t("Published", { context: "table header" })}
</TableCell> </TableCell>
<TableCell className={classes.colActions} />
</TableHead> </TableHead>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
colSpan={6} colSpan={numberOfColumns}
hasNextPage={maybe(() => pageInfo.hasNextPage)} hasNextPage={maybe(() => pageInfo.hasNextPage)}
onNextPage={onNextPage} onNextPage={onNextPage}
hasPreviousPage={maybe(() => pageInfo.hasPreviousPage)} hasPreviousPage={maybe(() => pageInfo.hasPreviousPage)}
@ -132,22 +159,23 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
disabled={disabled} disabled={disabled}
disableClickPropagation
onChange={() => toggle(product.id)} onChange={() => toggle(product.id)}
/> />
</TableCell> </TableCell>
<TableCellAvatar <TableCellAvatar
className={classes.colName}
thumbnail={maybe(() => product.thumbnail.url)} thumbnail={maybe(() => product.thumbnail.url)}
/> >
<TableCell>
{maybe<React.ReactNode>(() => product.name, <Skeleton />)} {maybe<React.ReactNode>(() => product.name, <Skeleton />)}
</TableCell> </TableCellAvatar>
<TableCell> <TableCell className={classes.colType}>
{maybe<React.ReactNode>( {maybe<React.ReactNode>(
() => product.productType.name, () => product.productType.name,
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell className={classes.colPublished}>
{maybe( {maybe(
() => ( () => (
<StatusLabel <StatusLabel
@ -162,7 +190,7 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
<Skeleton /> <Skeleton />
)} )}
</TableCell> </TableCell>
<TableCell className={classes.iconCell}> <TableCell className={classes.colActions}>
<IconButton <IconButton
disabled={!product} disabled={!product}
onClick={event => onProductUnassign(product.id, event)} onClick={event => onProductUnassign(product.id, event)}
@ -176,7 +204,9 @@ const CollectionProducts = withStyles(styles, { name: "CollectionProducts" })(
() => ( () => (
<TableRow> <TableRow>
<TableCell /> <TableCell />
<TableCell colSpan={6}>{i18n.t("No products found")}</TableCell> <TableCell colSpan={numberOfColumns}>
{i18n.t("No products found")}
</TableCell>
</TableRow> </TableRow>
) )
)} )}

View file

@ -1,4 +1,4 @@
import * as React from "react"; import React from "react";
import { getMutationProviderData } from "../../misc"; import { getMutationProviderData } from "../../misc";
import { PartialMutationProviderOutput } from "../../types"; import { PartialMutationProviderOutput } from "../../types";

View file

@ -22,6 +22,46 @@ export const collections: CollectionList_collections_edges_node[] = [
__typename: "ProductCountableConnection", __typename: "ProductCountableConnection",
totalCount: 4 totalCount: 4
} }
},
{
__typename: "Collection",
id: "Q29sbGVjdGlvbjoz",
isPublished: true,
name: "Vintage vibes",
products: {
__typename: "ProductCountableConnection",
totalCount: 4
}
},
{
__typename: "Collection",
id: "Q29sbGVjdGlvbjoa",
isPublished: true,
name: "Merry Christmas",
products: {
__typename: "ProductCountableConnection",
totalCount: 4
}
},
{
__typename: "Collection",
id: "Q29sbGVjdGlvbjob",
isPublished: true,
name: "80s Miami",
products: {
__typename: "ProductCountableConnection",
totalCount: 4
}
},
{
__typename: "Collection",
id: "Q29sbGVjdGlvbjoc",
isPublished: true,
name: "Yellow Submarine 2019",
products: {
__typename: "ProductCountableConnection",
totalCount: 4
}
} }
]; ];
export const collection: ( export const collection: (

View file

@ -1,5 +1,5 @@
import { parse as parseQs } from "qs"; import { parse as parseQs } from "qs";
import * as React from "react"; import React from "react";
import { Route, RouteComponentProps, Switch } from "react-router-dom"; import { Route, RouteComponentProps, Switch } from "react-router-dom";
import { WindowTitle } from "../components/WindowTitle"; import { WindowTitle } from "../components/WindowTitle";

View file

@ -1,5 +1,5 @@
import { stringify as stringifyQs } from "qs"; import { stringify as stringifyQs } from "qs";
import * as urlJoin from "url-join"; import urlJoin from "url-join";
import { BulkAction, Dialog, Pagination } from "../types"; import { BulkAction, Dialog, Pagination } from "../types";

View file

@ -1,4 +1,4 @@
import * as React from "react"; import React from "react";
import { WindowTitle } from "@saleor/components/WindowTitle"; import { WindowTitle } from "@saleor/components/WindowTitle";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";

View file

@ -1,6 +1,6 @@
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import DialogContentText from "@material-ui/core/DialogContentText"; import DialogContentText from "@material-ui/core/DialogContentText";
import * as React from "react"; import React from "react";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import AssignProductDialog from "@saleor/components/AssignProductDialog"; import AssignProductDialog from "@saleor/components/AssignProductDialog";
@ -287,14 +287,12 @@ export const CollectionDetails: React.StatelessComponent<
open={params.action === "assign"} open={params.action === "assign"}
onFetch={search} onFetch={search}
loading={result.loading} loading={result.loading}
onClose={() => navigate(collectionUrl(id), true, true)} onClose={closeModal}
onSubmit={formData => onSubmit={products =>
assignProduct.mutate({ assignProduct.mutate({
...paginationState, ...paginationState,
collectionId: id, collectionId: id,
productIds: formData.products.map( productIds: products.map(product => product.id)
product => product.id
)
}) })
} }
products={maybe(() => products={maybe(() =>

View file

@ -2,18 +2,19 @@ import Button from "@material-ui/core/Button";
import DialogContentText from "@material-ui/core/DialogContentText"; import DialogContentText from "@material-ui/core/DialogContentText";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import * as React from "react"; import React from "react";
import ActionDialog from "@saleor/components/ActionDialog"; import ActionDialog from "@saleor/components/ActionDialog";
import useBulkActions from "@saleor/hooks/useBulkActions"; import useBulkActions from "@saleor/hooks/useBulkActions";
import useListSettings from "@saleor/hooks/useListSettings";
import useNavigator from "@saleor/hooks/useNavigator"; import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import usePaginator, { import usePaginator, {
createPaginationState createPaginationState
} from "@saleor/hooks/usePaginator"; } from "@saleor/hooks/usePaginator";
import { PAGINATE_BY } from "../../config"; import i18n from "@saleor/i18n";
import i18n from "../../i18n"; import { getMutationState, maybe } from "@saleor/misc";
import { getMutationState, maybe } from "../../misc"; import { ListViews } from "@saleor/types";
import CollectionListPage from "../components/CollectionListPage/CollectionListPage"; import CollectionListPage from "../components/CollectionListPage/CollectionListPage";
import { import {
TypedCollectionBulkDelete, TypedCollectionBulkDelete,
@ -43,6 +44,9 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions( const { isSelected, listElements, reset, toggle, toggleAll } = useBulkActions(
params.ids params.ids
); );
const { updateListSettings, settings } = useListSettings(
ListViews.COLLECTION_LIST
);
const closeModal = () => const closeModal = () =>
navigate( navigate(
@ -62,7 +66,7 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
}) })
); );
const paginationState = createPaginationState(PAGINATE_BY, params); const paginationState = createPaginationState(settings.rowNumber, params);
return ( return (
<TypedCollectionListQuery displayLoader variables={paginationState}> <TypedCollectionListQuery displayLoader variables={paginationState}>
{({ data, loading, refetch }) => { {({ data, loading, refetch }) => {
@ -129,8 +133,10 @@ export const CollectionList: React.StatelessComponent<CollectionListProps> = ({
collections={maybe(() => collections={maybe(() =>
data.collections.edges.map(edge => edge.node) data.collections.edges.map(edge => edge.node)
)} )}
settings={settings}
onNextPage={loadNextPage} onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage} onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
pageInfo={pageInfo} pageInfo={pageInfo}
onRowClick={id => () => navigate(collectionUrl(id))} onRowClick={id => () => navigate(collectionUrl(id))}
toolbar={ toolbar={

View file

@ -9,8 +9,8 @@ import {
withStyles, withStyles,
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import * as classNames from "classnames"; import classNames from "classnames";
import * as React from "react"; import React from "react";
import i18n from "../../i18n"; import i18n from "../../i18n";
import ConfirmButton, { import ConfirmButton, {

View file

@ -5,14 +5,15 @@ import {
WithStyles WithStyles
} from "@material-ui/core/styles"; } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import * as React from "react"; import React from "react";
import { AddressTypeInput } from "../../customers/types"; import { AddressTypeInput } from "@saleor/customers/types";
import i18n from "../../i18n"; import i18n from "@saleor/i18n";
import { maybe } from "../../misc"; import { FormErrors } from "@saleor/types";
import { FormErrors } from "../../types";
import FormSpacer from "../FormSpacer"; import FormSpacer from "../FormSpacer";
import SingleAutocompleteSelectField from "../SingleAutocompleteSelectField"; import SingleAutocompleteSelectField, {
SingleAutocompleteChoiceType
} from "../SingleAutocompleteSelectField";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@ -24,24 +25,25 @@ const styles = (theme: Theme) =>
}); });
interface AddressEditProps extends WithStyles<typeof styles> { interface AddressEditProps extends WithStyles<typeof styles> {
countries?: Array<{ countries: SingleAutocompleteChoiceType[];
code: string; countryDisplayValue: string;
label: string;
}>;
data: AddressTypeInput; data: AddressTypeInput;
disabled?: boolean; disabled?: boolean;
errors: FormErrors<keyof AddressTypeInput>; errors: FormErrors<keyof AddressTypeInput>;
onChange(event: React.ChangeEvent<any>); onChange(event: React.ChangeEvent<any>);
onCountryChange(event: React.ChangeEvent<any>);
} }
const AddressEdit = withStyles(styles, { name: "AddressEdit" })( const AddressEdit = withStyles(styles, { name: "AddressEdit" })(
({ ({
classes, classes,
countries, countries,
countryDisplayValue,
data, data,
disabled, disabled,
errors, errors,
onChange onChange,
onCountryChange
}: AddressEditProps) => ( }: AddressEditProps) => (
<> <>
<div className={classes.root}> <div className={classes.root}>
@ -152,16 +154,14 @@ const AddressEdit = withStyles(styles, { name: "AddressEdit" })(
<div> <div>
<SingleAutocompleteSelectField <SingleAutocompleteSelectField
disabled={disabled} disabled={disabled}
displayValue={countryDisplayValue}
error={!!errors.country} error={!!errors.country}
helperText={errors.country} helperText={errors.country}
label={i18n.t("Country")} label={i18n.t("Country")}
name="country" name="country"
onChange={onChange} onChange={onCountryChange}
value={data.country} value={data.country}
choices={maybe( choices={countries}
() => countries.map(c => ({ ...c, value: c.code })),
[]
)}
InputProps={{ InputProps={{
autoComplete: "off" autoComplete: "off"
}} }}

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