diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..b2188ec1be6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,31 @@ +# Each line is a file pattern followed by one or more owners. +# Learn more at: https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners + +# All .rb files +*.rb @dshevtsov + +# All .rake files +*.rake @dshevtsov + +# All .sh files +*.sh @dshevtsov + +# All files at the root directory, non-recursively. +/* @dshevtsov + +# All files at the /.github/linters/ directory and in sub-directories. +/.github/linters/ @dshevtsov + +# All files at the /.github/workflows/ directory and in sub-directories. +/.github/workflows/ @dshevtsov + +# All files at the /.github/bin/ directory and in sub-directories. +/bin/ @dshevtsov + +/docker-compose.yaml @hguthrie + +LICENSE.txt @jeff-matthews + +COPYING.txt @jeff-matthews + +CODEOWNERS @jeff-matthews @dshevtsov diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..a02084614d5 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Magento Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contribute to a positive environment for our project and community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best, not just for us as individuals but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies when an individual is representing the project or its community both within project spaces and in public spaces. Examples of representing a project or community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by first contacting the project team at `engcom@adobe.com`. Oversight of Adobe projects is handled by the Adobe Open Source Office, which has final say in any violations and enforcement of this Code of Conduct and can be reached at `Grp-opensourceoffice@adobe.com`. All complaints will be reviewed and investigated promptly and fairly. + +The project team must respect the privacy and security of the reporter of any incident. + +Project maintainers who do not follow or enforce the Code of Conduct may face temporary or permanent repercussions as determined by other members of the project's leadership or the Adobe Open Source Office. + +## Enforcement Guidelines + +Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem to be in violation of this Code of Conduct: + +### 1. Correction + +Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +Consequence: A private, written warning from project maintainers describing the violation and why the behavior was unacceptable. A public apology may be requested from the violator before any further involvement in the project by violator. + +### 2. Warning + +Community Impact: A relatively minor violation through a single incident or series of actions. + +Consequence: A written warning from project maintainers that includes stated consequences for continued unacceptable behavior. Violator must refrain from interacting with the people involved for a specified period of time as determined by the project maintainers, including, but not limited to, unsolicited interaction with those enforcing the Code of Conduct through channels such as community spaces and social media. Continued violations may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +Community Impact: A more serious violation of community standards, including sustained unacceptable behavior. + +Consequence: A temporary ban from any interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Failure to comply with the temporary ban may lead to a permanent ban. + +### 4. Permanent Ban + +Community Impact: Demonstrating a consistent pattern of violation of community standards or an egregious violation of community standards, including, but not limited to, sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +Consequence: A permanent ban from any interaction with the community. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.1, +available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000000..478cf20deaa --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,153 @@ +## Contribute to Magento DevDocs + +Share your knowledge with the community by contributing to Magento DevDocs! +You can contribute by creating an issue or pull request (PR) on our [DevDocs](https://github.com/magento/devdocs) GitHub repository. +We welcome all types of contributions; from minor typo fixes to new topics. + +DevDocs staff members and [Community Maintainers](https://devdocs.magento.com/contributor-guide/contributors.html#/community-maintainers) review issues and pull requests on a regular basis. We do our best to address all issues as soon as possible, but working through the backlog takes time. We appreciate your patience. + +## Rewards for contributions + +If you write and contribute a full topic, we will add your name (or your company's name) at the top of the DevDocs page and link it to your blog or website! + +If you contribute a new topic or a major update to a topic, your GitHub username will be added to a description of the update in our [What's New topic](https://devdocs.magento.com/whats-new.html). + +## Get started + +![Get started workflow](https://devdocs.magento.com/common/images/contribute-prerequisites.png) + +1. Make sure you have a [GitHub account](https://github.com/signup/free). + + **Note for partners:** Add [2FA](https://devdocs.magento.com/contributor-guide/contributing.html#two-factor) protection when contributing to Magento repositories. + +1. [Fork](https://help.github.com/articles/fork-a-repo/) the [DevDocs repository](https://github.com/magento/devdocs). Remember to [sync your fork](https://help.github.com/articles/syncing-a-fork/) and update branches as needed. +1. Review the [DevDocs guidelines](#contribution-guidelines). + +**Note:** If you use a fork instead of a branch, please set permissions to allow maintainers to edit and update the PR. See [Allowing changes to a pull request branch created from a fork](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) in the _GitHub documentation_. + +## Contribute documentation + +The following diagram shows the contribution workflow: + +![Contributing workflow](https://devdocs.magento.com/common/images/contribute-write-submit-pr.png) + +**Tip!** If you are not sure where to start contributing, search for issues with the [`help wanted`](https://github.com/magento/devdocs/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22), [`good first issue`](https://github.com/magento/devdocs/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), and [`groomed`](https://github.com/magento/devdocs/issues?q=is:issue+is:open+label:%22groomed%22) labels. These issues receive higher priority for processing. + +### Create a branch + +1. Create a new branch from your fork using a name that best describes the work or references a GitHub issue number. +1. Edit or create markdown (`.md`) files in your branch. +1. Push your branch to your fork. + +### Create a pull request + +1. Create a pull request to the [magento/devdocs](https://github.com/magento/devdocs) repository. + + In general, you should use `master` as the base branch when creating a PR. If your contribution is related to a release that is in progress, use a version-specific integration branch, such as `develop`. + +1. Complete the pull request template. Review the [Pull Request Process page](https://github.com/magento/devdocs/wiki/Pull-Request-Process) to learn how to complete a PR (with info about completing the template, adding a `whatsnew`, and more.) + + **We will close your pull request if you do not complete the template.** + +1. After creating a pull request, a DevDocs staff member or maintainer will review it and may ask you to make revisions. + + **We will close your pull request if you do not respond to feedback in two weeks.** + +**Note:** If you have not signed the [Adobe Contributor License Agreement](https://opensource.adobe.com/cla.html), the pull request provides a link. You must sign the CLA before we can accept your contribution. + +## Contribution guidelines + +The following guidelines may answer most of your questions and help you get started: + +- Write content using Markdown. See the [Templates](#templates) section for examples. +- Review existing [pull requests](https://github.com/magento/devdocs/pulls) and [issues](https://github.com/magento/devdocs/issues) to avoid duplicating work. +- For large contributions, or changes that include multiple files, [open an issue](#report-an-issue) and discuss it with us first. This helps prevent duplicate or unnecessary work. +- Do not make global find-and-replace changes without first [creating an issue](https://github.com/magento/devdocs/issues/new/choose) and discussing it with us. Global changes can have unintended consequences. +- Do not make changes to content in the [`_data/codebase`](https://github.com/magento/devdocs/tree/master/src/_data/codebase) directory, which contains auto-generated data from Magento source code. Any manual changes will be lost when the file regenerates. +- Combine multiple small changes (such as minor editorial and technical changes) into a single pull request. This helps us efficiently and effectively facilitate your contribution. +- Familiarize yourself with the organization and conventions of our existing documentation before creating a pull request. Changes that are consistent with our style and conventions have a higher acceptance rate. + + - If you need to update the site navigation, ask for help in Slack ([#devdocs](https://magentocommeng.slack.com/messages/CAN932A3H)). + +- Ensure that you update the correct version(s) of documentation (v2.3). If you are not sure what directory to put your content in, just do your best. We can help re-locate it (if necessary) during the review process. +- Review your work for basic typos, formatting errors, or ambiguous sentences before opening a pull request. +- Revise pull requests according to review feedback. We will close pull requests that require an inordinate amount of time to review and process (especially for minor changes) if you fail to make revisions according to review feedback. +- Do not directly contact DevDocs team members or maintainers on Slack to review your pull request unless it has been open for more than five days. We have a process and queue for pull requests that everyone must follow. +- Get recognized on the DevDocs web site for writing new topics! Add your name and a link to your company website or GitHub profile to the file metadata so that we can display it on the page. See [Edit metadata](#edit-metadata). +- We no longer recognize individual community members who contribute features to the Magento 2 codebase in the corresponding feature topic(s) on the DevDocs website. Magento recognizes these contributions in more appropriate channels (for example, the [Magento DevBlog](https://community.magento.com/t5/Magento-DevBlog/bg-p/devblog)). + +## Tips for writing content + +Use the following guidelines to help you with the writing process: + +- Focus your efforts on providing useful information for your fellow Magento developers and community members. For example, consider providing or revising code samples, important notes, and clarifying vague or ambiguous content. +- Define the goal of your topic. What exactly do you want to teach the reader? +- Make the title of your topic reflect the content. +- Keep your sentences concise. Separate conceptual information from procedural steps. +- Batch several small changes into a single pull request (instead of separate ones) to ensure your contributions are approved and merged quickly. Have several typo fixes across several areas of documentation? Combine them into a single PR. +- Remember to write in present tense, use the second person, and use active voice (not passive). For example, _"The log captures commands, output..."_. +- Use notes to alert readers about important details. +- Use cross-references to other topics sparingly. We can help you with the syntax if it is not clear. The template provides an example you can use. +- Need help with markdown? See our [templates](#templates). + +### Review changes locally + +_(Optional)_ To review your changes in HTML output, follow the instructions in the [README](https://github.com/magento/devdocs/blob/master/README.md) to build the devdocs site locally using Jekyll. + +### Templates + +We provide templates to help you get started writing new content and understanding Markdown formatting: + +- **General topic template** - [Markdown](https://github.com/magento/devdocs/blob/master/contributor-guide/templates/basic_template.md) | [HTML](https://devdocs.magento.com/contributor-guide/templates/basic_template.html): This is a template for writing any topic with example formats and styles. +- **Tutorial templates**: These templates provide example formats and styles for step-by-step instructions (like how-tos). Each file adds navigation buttons when content is generated. Templates include: + - First introduction topic - [Markdown](https://github.com/magento/devdocs/blob/master/contributor-guide/templates/tutorial-template-first.md) | [HTML](https://devdocs.magento.com/contributor-guide/templates/tutorial-template-first.html): Introduction to a tutorial for prerequisites and listing steps + - Middle topic - [Markdown](https://github.com/magento/devdocs/blob/master/contributor-guide/templates/tutorial-template-middle.md) | [HTML](https://devdocs.magento.com/contributor-guide/templates/tutorial-template-middle.html): Use for each step in a tutorial. + - Final step topic - [Markdown](https://github.com/magento/devdocs/blob/master/contributor-guide/templates/tutorial-template-last.md) | [HTML](https://devdocs.magento.com/contributor-guide/templates/tutorial-template-last.html): Use for the last step of the tutorial. + +### Edit metadata + +The Markdown (.md) file's metadata is a set of YAML key-value pairs. The metadata section is located at the top of each file. For more info, see the [Basic Template](https://devdocs.magento.com/contributor-guide/templates/basic_template.html). + +```yaml +--- +group: +title: +contributor_name: +contributor_link: +--- +``` + +> Key-value pair reference: + +| Property | Description | +| ------------- | ---------- | +| `group` | Defines the topic's guide or section. Use the table of contents `.yml` file name. This loads your left-side navigation. We will help during the PR process to add new files to the `.yml` file. | +| `title` | Sets the title of the page in the HTML metadata and the main title on the page. | +| `contributor_name` | Sets the name of the contributor who wrote the topic and displays it on the page. | +| `contributor_link` | Creates a link to the contributor's GitHub profile or company website. | + +## Report an issue + +If you find a typo or errors in Magento DevDocs, you can either fix it with a pull request (as described above) or you can report it by creating an issue in the DevDocs GitHub repository. + +You must complete the issue template. We will close your issue if you fail to complete the template. Enter as much information as you can, including content corrections, steps to reproduce, command or code updates, or questions for clarifications. + +**Note:** Check the existing [issues](https://github.com/magento/devdocs/issues) on GitHub to see if someone has already reported the issue. + +You can provide feedback using the following options: + +- Have general feedback? Create an issue on [GitHub DevDocs](https://github.com/magento/devdocs/issues/new/choose). +- Have feedback on a specific DevDocs page? Click the **Give us feedback** link at the top right of the page to report on the currently open topic. + + ![Report an issue](https://devdocs.magento.com/common/images/contribute-feedback-link.png) + +- Have a Community code contribution that needs documentation? Create an issue to [request DevDocs content](https://github.com/magento/devdocs/issues/new?template=COMMUNITY_ISSUE_TEMPLATE.md). + +## Contact DevDocs + +Have a question? Need help? Magento DevDocs, Maintainers, and other Contributors are available through: + +- [Slack](https://magentocommeng.slack.com/archives/CAN932A3H) ([Join us](https://opensource.magento.com/slack)) +- [Twitter @AdobeCommrcDocs](https://twitter.com/AdobeCommrcDocs) + +Thank you for contributing your brilliance to Magento DevDocs!! diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..303e5651398 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Help Center + url: https://support.magento.com/hc/en-us + about: Find help with issues unrelated to documentation here. + - name: Community resources + url: https://developer.adobe.com/open/magento + about: Find more resources here. + - name: Contact the team + url: https://magentocommeng.slack.com/messages/CAN932A3H + about: Ask and answer questions directly with the team. diff --git a/.github/ISSUE_TEMPLATE/incorrect_topic.yaml b/.github/ISSUE_TEMPLATE/incorrect_topic.yaml new file mode 100644 index 00000000000..09f38bfba70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/incorrect_topic.yaml @@ -0,0 +1,35 @@ +name: Incorrect or unclear topic +description: "Unclear or incorrect documentation: ambiguous guidelines, wrong or obsolete examples, typos, etc." +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to report this issue! + This request should only relate to the content of the devdocs.magento.com website. + If you are having an issue with the actual product (as opposed to the docs), search the [Help Center](https://support.magento.com/hc/en-us) or report it as a [codebase issue](https://devdocs.magento.com/contributor-guide/contributing.html#report). + Requests that do not comply with our Code of Conduct or do not contain enough information may be closed at the maintainers' discretion. + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the documentation you are requesting. + options: + - label: I have searched the existing issues + required: true + - type: input + attributes: + label: Which topic? + description: A link to the topic that needs clarification or correction + placeholder: "Example: https://devdocs.magento.com/guides/v2.4/install-gde/system-requirements.html" + validations: + required: true + - type: textarea + attributes: + label: What's wrong with the content? + validations: + required: true + - type: textarea + attributes: + label: What changes do you propose? + - type: textarea + attributes: + label: Anything else that can help to cover this? diff --git a/.github/ISSUE_TEMPLATE/missing_content.yaml b/.github/ISSUE_TEMPLATE/missing_content.yaml new file mode 100644 index 00000000000..de7750c5fbe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/missing_content.yaml @@ -0,0 +1,28 @@ +name: Missing content +description: Undocumented feature, service, command, API, UI component, procedure, etc. +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to report this issue! + This request should only relate to the content of the devdocs.magento.com website. + If you are having an issue with the actual product (as opposed to the docs), search the [Help Center](https://support.magento.com/hc/en-us) or report it as a [codebase issue](https://devdocs.magento.com/contributor-guide/contributing.html#report). + Requests that do not comply with our Code of Conduct or do not contain enough information may be closed at the maintainers' discretion. + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the documentation you are requesting. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: What's missing? + placeholder: | + - In the or ... + - Missing explanation of how ... works. + - Missing steps or guidelines for .... + - Missing code samples to demonstrate .... + - Something else ... + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/site_bug.yaml b/.github/ISSUE_TEMPLATE/site_bug.yaml new file mode 100644 index 00000000000..30a5e49e820 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/site_bug.yaml @@ -0,0 +1,52 @@ +name: Site bug +description: Something isn't working on devdocs.magento.com or when building the website locally. +labels: ["bug"] +assignees: + - dshevtsov +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to report this issue! + This bug report should only relate to the devdocs website itself or non-content issues such as when building locally or running rake commands. + If you are having an issue with the actual product (as opposed to the docs), search the [Help Center](https://support.magento.com/hc/en-us) or report it as a [codebase issue](https://devdocs.magento.com/contributor-guide/contributing.html#report). + Issues that do not comply with our Code of Conduct or do not contain enough information may be closed at the maintainers' discretion. + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Current behavior + description: | + Tell us what happened. Include error messages and issues. + validations: + required: true + - type: textarea + attributes: + label: Expected behavior + description: | + Tell us what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce + description: | + Provide a set of clear steps to reproduce this bug. + validations: + required: true + - type: textarea + attributes: + label: Environment + description: | + Describe your environment. + Provide all the details that will help us to reproduce the bug. + value: | + - Browser: + - OS: + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/site_feature.yaml b/.github/ISSUE_TEMPLATE/site_feature.yaml new file mode 100644 index 00000000000..2d9e6da9918 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/site_feature.yaml @@ -0,0 +1,38 @@ +name: Site feature request +description: Propose a new functionality or an improvement of the devdocs.magento.com website or of the local development tools. +labels: ["Site Improvements"] +assignees: + - dshevtsov +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to report this issue! + This feature request should only relate to the devdocs website itself or other non-content request such as rake commands. + If you are having an issue with the actual product (as opposed to the docs), search the [Help Center](https://support.magento.com/hc/en-us) or report it as a [codebase issue](https://devdocs.magento.com/contributor-guide/contributing.html#report). + Requests that do not comply with our Code of Conduct or do not contain enough information may be closed at the maintainers' discretion. + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the feature you are requesting. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: What feature should be added? + validations: + required: true + - type: textarea + attributes: + label: What is the expected behavior? + validations: + required: true + - type: textarea + attributes: + label: How will this feature improve the user experience? + validations: + required: true + - type: textarea + attributes: + label: What would a solution for this issue look like? diff --git a/.github/MAINTAINER_GUIDELINES.md b/.github/MAINTAINER_GUIDELINES.md new file mode 100644 index 00000000000..894c26991c5 --- /dev/null +++ b/.github/MAINTAINER_GUIDELINES.md @@ -0,0 +1,55 @@ +# DevDocs maintainer guidelines + +In general, the same [guidelines](https://devdocs.magento.com/contributor-guide/maintainers.html) for [`magento/magento2`](https://github.com/magento/magento2) maintainers apply to DevDocs maintainers. However, there are some additional guidelines specific to DevDocs that will help you as a maintainer. + +## General expectations + +- Self assign issues/pull requests (mostly pull requests). See the [Pull Request Process wiki topic](https://github.com/magento/devdocs/wiki/Pull-Request-Process) to learn more. + - Review and approve, or request changes. + - Enforce the use of the issue/pull request template. + - Ask contributors to link to the code base to validate documentation updates when applicable. + - Ask contributors not to contribute to unsupported versions of documentation when applicable. + - Ask contributors to add a [`whatsnew`](https://github.com/magento/devdocs/wiki/Pull-Request-Process) to their `New Topic`, `Major Update`, or `Technical` labeled PRs. +- If a DevDocs maintainer creates a pull request, it must be reviewed by another maintainer or DevDocs admin. + +## Labels + +Labels are important because they help us identify pull requests by type and ensure that contributors receive points and recognition. See [DevDocs awards and points](https://devdocs.magento.com/contributor-guide/contributing.html#devdocs-awards-and-points). + +Here is a brief summary of the most important labels: + +- `New Topic`: Entirely new documents +- `Major Update`: Significant new info (for example, a new section in an existing topic) +- `Technical`: Minor changes to technical content, code, processes, naming conventions +- `Editorial`: Typos, grammatical inconsistencies, or minor rewrites + +PRs with the `Internal Dev` label were created by Magento/Adobe employees and will be handled by the Documentation team only. + +We also use version labels when appropriate (for example, 2.3.x). + +As a maintainer, we expect you to add or remove labels according to these guidelines. + +See https://github.com/magento/devdocs/labels for all labels and their descriptions. + +## Communicating internally and externally + +There may be instances in which a maintainer has questions about a specific PR or issue. There are proper channels for communicating internally (Magento) and externally (contributors): + +- Externally: Questions, revisions, or other conversation with the contributor must happen within the applicable PR or issue. Tag the contributor, if needed, to get their attention. +- Internally: Questions for the Magento Documentation team about a PR or issue can happen as a comment in the applicable PR or issue or within the #devdocs_maintainers channel in Magento Community Engineering Slack. If your question pertains to a specific team member, you can tag their name to initiate the conversation. + +## Testing + +We use a private CI/CD stack and do not provide access to it. + +Every pull request to the `master` branch must pass tests before it can be merged. When a pull request is ready for tests, a member of the [`devdocs-admins`](https://github.com/orgs/magento/teams/devdocs-admins) team must add the test trigger phrase to the pull request as a comment. The trigger phrase is "_running tests_". By approving a pull request, you are signalling to an admin that the pull request is ready for tests. + +## Projects + +We use several projects to help organize issues and pull requests. You can add existing issues and pull requests to these projects as you see fit. + +https://github.com/magento/devdocs/projects + +## Style + +We prefer Markdown over HTML (in most cases). You can use [kramdown](https://kramdown.gettalong.org/syntax.html) syntax for more markup features and [Liquid](https://jekyllrb.com/docs/liquid/) for in-topic scripting. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..f048a6fd01d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ +## Purpose of this pull request + +This pull request (PR) ... + +## Affected DevDocs pages + + + +- ... + +## Links to Magento source code + + + +- ... + + diff --git a/.github/linters/.markdownlint.json b/.github/linters/.markdownlint.json new file mode 100644 index 00000000000..922a907842c --- /dev/null +++ b/.github/linters/.markdownlint.json @@ -0,0 +1,25 @@ +{ + "default": false, + "MD001": true, + "MD003": { "style": "atx" }, + "MD004": true, + "MD005": true, + "MD006": true, + "MD007": { "indent": 3 }, + "MD009": true, + "MD010": true, + "MD012": true, + "MD018": true, + "MD019": true, + "MD023": true, + "MD025": true, + "MD029": true, + "MD030": { "ol_single": 1, "ol_multi": 1, "ul_single": 2, "ul_multi": 2 }, + "MD032": true, + "MD035": true, + "MD036": true, + "MD037": true, + "MD038": true, + "MD040": true, + "MD046": { "style": "fenced"} +} diff --git a/.github/linters/.ruby-lint.yml b/.github/linters/.ruby-lint.yml new file mode 100644 index 00000000000..29b77e8d6b5 --- /dev/null +++ b/.github/linters/.ruby-lint.yml @@ -0,0 +1,11 @@ +--- +####################### +# Rubocop Config file # +####################### + +inherit_gem: + rubocop-github: + - config/default.yml + +Style/StringLiterals: + EnforcedStyle: single_quotes diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 00000000000..14995a92a49 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,75 @@ +--- +########################### +########################### +## Linter GitHub Actions ## +########################### +########################### +name: Lint Code Base + +# +# Documentation: +# https://help.github.com/en/articles/workflow-syntax-for-github-actions +# + +################################### +# Start the job on a pull request # +################################### +on: + pull_request + +############### +# Set the Job # +############### +permissions: + contents: read + +jobs: + mdl: + name: mdl + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + - name: Install mdl gem + run: gem install mdl + - name: Run mdl + run: mdl --style=_checks/styles/style-rules-prod --ignore-front-matter --git-recurse -- . + super-lint: + # Set the agent to run on + runs-on: ubuntu-latest + + ################## + # Load all steps # + ################## + steps: + ########################## + # Checkout the code base # + ########################## + - name: Checkout Code + uses: actions/checkout@v3 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + + ################################ + # Run Linter against code base # + ################################ + - name: Lint Code Base + uses: github/super-linter/slim@v4.9.7 + env: + VALIDATE_ALL_CODEBASE: false + DEFAULT_BRANCH: master + VALIDATE_BASH: true + VALIDATE_BASH_EXEC: true + VALIDATE_CSS: true + VALIDATE_DOCKERFILE_HADOLINT: true + VALIDATE_GITHUB_ACTIONS: true + VALIDATE_GITLEAKS: true + VALIDATE_JAVASCRIPT_ES: true + VALIDATE_JSON: true + VALIDATE_MARKDOWN: true + MARKDOWN_CONFIG_FILE: .markdownlint.json + VALIDATE_RUBY: true + VALIDATE_SHELL_SHFMT: true + VALIDATE_YAML: true diff --git a/.gitignore b/.gitignore index e1e24bb650f..10339208556 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,30 @@ -_site/ - -======= -# OS generated files # -###################### +# OS and IDE generated files # +############################## +/*.db .DS_Store -.DS_Store? -/.DS_Store -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db +.vscode +.history +/.idea # Config and output files # ########################### -_site -CNAME -_config.yml -/Readme.md -======= -ignore -/_config.yml -/_config.steve.yml -/_config.diane.yml -/_config.brad.yml -/_config.olena.yml -/_config.sasha.yml -/_config.tana.yml -/_config.public.yml -/*.yml -/gen_public.bat -/jk.bat + +/_site/ + +_config.local.yml +.jekyll-metadata +.jekyll-cache + *.bat +/tmp/ +/vendor/ +/node_modules/ + +_algolia_api_key + +# Docs from different repositories # +################################### + +/src/mbi/ +/src/page-builder/ +/src/mftf/ diff --git a/.imgbotconfig b/.imgbotconfig new file mode 100644 index 00000000000..000f0d4aa25 --- /dev/null +++ b/.imgbotconfig @@ -0,0 +1,6 @@ +{ + "ignoredFiles": [ + "*.svg" + ], + "aggressiveCompression": false +} \ No newline at end of file diff --git a/.markdownlint.json b/.markdownlint.json new file mode 120000 index 00000000000..600dde1bbb1 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1 @@ +.github/linters/.markdownlint.json \ No newline at end of file diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 00000000000..1af5b6fc06b --- /dev/null +++ b/.mdlrc @@ -0,0 +1,2 @@ +style '_checks/styles/style-rules-dev' +ignore_front_matter true diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000000..849c0c47ffc --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.7.5 diff --git a/.whatsup.yml b/.whatsup.yml new file mode 100755 index 00000000000..a8e81500b99 --- /dev/null +++ b/.whatsup.yml @@ -0,0 +1,23 @@ +# Parameters for a GitHub search query +base_branch: master + +repos: + - magento/devdocs + - magento-commerce/devdocs + +# Labels also will be used as a 'type' value in the output file +labels: + required: + - New Topic + - Major Update + optional: + - Technical + +# Format of output file +output_format: + - yaml +# - markdown + +magic_word: whatsnew + +membership: magento-commerce diff --git a/CNAME b/CNAME deleted file mode 100644 index c08de060664..00000000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -devdocs.magento.com diff --git a/COPYING.txt b/COPYING.txt new file mode 100644 index 00000000000..29e74763d63 --- /dev/null +++ b/COPYING.txt @@ -0,0 +1,9 @@ +Copyright © 2013-present Adobe Inc. + +Each Adobe source file included in this distribution is licensed under OSL 3.0 or the Adobe Commerce license. + +http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0) +Please see LICENSE.txt for the full text of the OSL 3.0 license or contact license@adobe.com for a copy. + +Subject to Licensee's payment of fees and compliance with the terms and conditions of the Adobe License, the Adobe License supersedes the OSL 3.0 license for each source file. +Please see LICENSE.txt for the full text of the Adobe License or visit http://magento.com/legal/terms/enterprise. diff --git a/Docfile.yml b/Docfile.yml new file mode 100644 index 00000000000..6b93f68f76a --- /dev/null +++ b/Docfile.yml @@ -0,0 +1,22 @@ +content_map: + +- + directory: src/mbi + repository: magento/devdocs-mbi + branch: master + filter: true +- + directory: src/mftf + repository: commerce-docs/magento2-functional-testing-framework-public + branch: migrated-docs + filter: true +- + directory: src/mftf/v2 + repository: commerce-docs/magento2-functional-testing-framework-public + branch: 2.x-develop + filter: true +- + directory: src/page-builder + repository: magento/pagebuilder-docs + branch: master + filter: true diff --git a/Gemfile b/Gemfile index 6bb9eb91db3..633d70e8fa0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,34 @@ -gem 'github-pages' +# frozen_string_literal: true + source 'https://rubygems.org' + +gem 'devdocs', git: 'https://github.com/commerce-docs/devdocs-theme.git' +gem 'jekyll', '4.2.2' +gem 'rake' +# gem 'whatsup_github' + +# gem 'wdm', platform: :mswin + +group :test do + gem 'html-proofer' + gem 'launchy' + # gem 'mdl' +end + +# group :optimization do +# gem 'image_optim' +# gem 'image_optim_pack' +# end + +group :jekyll_plugins do + # gem 'jekyll-algolia', '~> 1.0' + gem 'jekyll-optional-front-matter' + gem 'jekyll-redirect-from' + gem 'jekyll-relative-links' + gem 'jekyll-sitemap' + gem 'jekyll-titles-from-headings' +end + +group :resolutions do + gem 'ffi', '1.15.5' +end \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 8a7badb6596..7c5117154af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,114 +1,132 @@ +GIT + remote: https://github.com/commerce-docs/devdocs-theme.git + revision: 04462e71c495bb63ecdf0fd61ca679b116da9026 + specs: + devdocs (20) + jekyll (>= 4.0) + GEM + remote: https://rubygems.org/ specs: - RedCloth (4.2.9) - activesupport (4.1.6) - i18n (~> 0.6, >= 0.6.9) - json (~> 1.7, >= 1.7.7) - minitest (~> 5.1) - thread_safe (~> 0.1) - tzinfo (~> 1.1) - blankslate (2.1.2.4) - celluloid (0.16.0) - timers (~> 4.0.0) - classifier-reborn (2.0.1) - fast-stemmer (~> 1.0) - coffee-script (2.3.0) - coffee-script-source - execjs - coffee-script-source (1.8.0) - colorator (0.1) - execjs (2.2.2) - fast-stemmer (1.0.2) - ffi (1.9.6) - gemoji (2.1.0) - github-pages (28) - RedCloth (= 4.2.9) - jekyll (= 2.4.0) - jekyll-coffeescript (= 1.0.0) - jekyll-mentions (= 0.1.3) - jekyll-redirect-from (= 0.6.2) - jekyll-sass-converter (= 1.2.0) - jekyll-sitemap (= 0.6.0) - jemoji (= 0.3.0) - kramdown (= 1.3.1) - liquid (= 2.6.1) - maruku (= 0.7.0) - pygments.rb (= 0.6.0) - rdiscount (= 2.1.7) - redcarpet (= 3.1.2) - hitimes (1.2.2) - html-pipeline (1.9.0) - activesupport (>= 2) - nokogiri (~> 1.4) - i18n (0.6.11) - jekyll (2.4.0) - classifier-reborn (~> 2.0) - colorator (~> 0.1) - jekyll-coffeescript (~> 1.0) - jekyll-gist (~> 1.0) - jekyll-paginate (~> 1.0) - jekyll-sass-converter (~> 1.0) - jekyll-watch (~> 1.1) - kramdown (~> 1.3) - liquid (~> 2.6.1) - mercenary (~> 0.3.3) - pygments.rb (~> 0.6.0) - redcarpet (~> 3.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + childprocess (5.0.0) + colorator (1.1.0) + concurrent-ruby (1.3.3) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + ethon (0.16.0) + ffi (>= 1.15.0) + eventmachine (1.2.7) + ffi (1.15.5) + forwardable-extended (2.6.0) + html-proofer (4.4.3) + addressable (~> 2.3) + mercenary (~> 0.3) + nokogiri (~> 1.13) + parallel (~> 1.10) + rainbow (~> 3.0) + typhoeus (~> 1.3) + yell (~> 2.0) + zeitwerk (~> 2.5) + http_parser.rb (0.8.0) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + jekyll (4.2.2) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (~> 2.0) + jekyll-watch (~> 2.0) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (~> 0.4.0) + pathutil (~> 0.9) + rouge (~> 3.0) safe_yaml (~> 1.0) - toml (~> 0.1.0) - jekyll-coffeescript (1.0.0) - coffee-script (~> 2.2) - jekyll-gist (1.1.0) - jekyll-mentions (0.1.3) - html-pipeline (~> 1.9.0) - jekyll (~> 2.0) - jekyll-paginate (1.1.0) - jekyll-redirect-from (0.6.2) - jekyll (~> 2.0) - jekyll-sass-converter (1.2.0) - sass (~> 3.2) - jekyll-sitemap (0.6.0) - jekyll-watch (1.1.1) - listen (~> 2.7) - jemoji (0.3.0) - gemoji (~> 2.0) - html-pipeline (~> 1.9) - jekyll (~> 2.0) - json (1.7.7) - kramdown (1.3.1) - liquid (2.6.1) - listen (2.7.11) - celluloid (>= 0.15.2) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - maruku (0.7.0) - mercenary (0.3.4) - minitest (5.4.2) - nokogiri (1.5.6) - parslet (1.5.0) - blankslate (~> 2.0) - posix-spawn (0.3.9) - pygments.rb (0.6.0) - posix-spawn (~> 0.3.6) - yajl-ruby (~> 1.1.0) - rb-fsevent (0.9.4) - rb-inotify (0.9.5) - ffi (>= 0.5.0) - rdiscount (2.1.7) - redcarpet (3.1.2) - safe_yaml (1.0.4) - sass (3.4.6) - thread_safe (0.3.4) - timers (4.0.1) - hitimes - toml (0.1.2) - parslet (~> 1.5.0) - tzinfo (1.2.2) - thread_safe (~> 0.1) - yajl-ruby (1.1.0) + terminal-table (~> 2.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.7.0) + jekyll (>= 3.3, < 5.0) + jekyll-sass-converter (2.2.0) + sassc (> 2.0.1, < 3.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + launchy (3.0.1) + addressable (~> 2.8) + childprocess (~> 5.0) + liquid (4.0.4) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + mini_portile2 (2.8.7) + nokogiri (1.15.6) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + nokogiri (1.15.6-arm64-darwin) + racc (~> 1.4) + nokogiri (1.15.6-x86_64-darwin) + racc (~> 1.4) + parallel (1.25.1) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (5.1.1) + racc (1.8.1) + rainbow (3.1.1) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rexml (3.3.6) + strscan + rouge (3.30.0) + safe_yaml (1.0.5) + sassc (2.4.0) + ffi (~> 1.9) + strscan (3.1.0) + terminal-table (2.0.0) + unicode-display_width (~> 1.1, >= 1.1.1) + typhoeus (1.4.1) + ethon (>= 0.9.0) + unicode-display_width (1.8.0) + yell (2.2.2) + zeitwerk (2.6.17) PLATFORMS + arm64-darwin-22 ruby + x86-mswin32 + x86_64-darwin-19 + x86_64-darwin-21 + x86_64-darwin-22 DEPENDENCIES - github-pages + devdocs! + ffi (= 1.15.5) + html-proofer + jekyll (= 4.2.2) + jekyll-optional-front-matter + jekyll-redirect-from + jekyll-relative-links + jekyll-sitemap + jekyll-titles-from-headings + launchy + rake + +BUNDLED WITH + 2.1.4 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000000..49dc8ec82eb --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/README.md b/README.md index e85c53407d2..169996e61be 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,162 @@ -Magento Developer Documentation -================================= +# Adobe Commerce Developer Documentation -Welcome! This site contains the latest Magento developer documentation for the developer beta release. +This project contains the source code of the Adobe Commerce and Magento Open Source developer documentation website for the latest 2.3 release. -To contribute, please fork the develop branch. We won't accept pull requests into master. +> **Important update** +> +> Adobe Commerce and Magento Open Source 2.4.x documentation has been migrated to Adobe sites. See our new landing pages to access the most current information. +> +>[Adobe Commerce Developer Documentation](https://developer.adobe.com/commerce/docs/) (Adobe Developer site)—Develop, customize, integrate, extend, and use advanced capabilities +> +>[Adobe Commerce Documentation](https://experienceleague.adobe.com/docs/commerce.html) (Adobe Experience League)—Plan, implement, operate, upgrade, and maintain your Commerce projects +> +> Some content was consolidated or moved to different guides. If you have trouble finding a topic, see the [Migrated topics](https://commerce-docs.github.io/devdocs-archive/migrated-topics.html). +> +> We welcome contributions to migrated content! You can find similar links to GitHub on the Adobe sites. +> +> The content at https://devdocs.magento.com is no longer supported since [the 2.3 release line has reached end of support](https://experienceleague.adobe.com/docs/commerce-operations/release/versions.html#2.3). -We're looking forward to hearing from you! +## Building this site + +This site is built by [Jekyll](https://jekyllrb.com/), which is an open-source tool developed in [Ruby](https://www.ruby-lang.org/en/). + +You can build the site locally in the following ways: + +- [Installing the project dependencies locally](#build-locally) (Mac, Linux) +- [Using Docker (docker-compose)](https://github.com/magento/devdocs/wiki/Build-DevDocs-with-Docker) (Mac, Linux, Windows) +- [Using a Vagrant virtual machine](https://github.com/commerce-docs/vagrant-for-magento-devdocs) (Mac, Linux, Windows) +- [Build DevDocs in Windows](https://github.com/magento/devdocs/wiki/Build-DevDocs-in-Windows) (Windows 7 & 10) +- [Building older versions of the documentation](https://github.com/magento/devdocs/wiki/Build-DevDocs-with-Docker) + +## Build locally + +You do not need to set up a webserver to serve the site locally. Jekyll will use its own webserver for this. + +### Set up Ruby + +Consider to set up the Ruby version defined in [.ruby-version](.ruby-version). +Ruby version manager such as [rvm](https://www.ruby-lang.org/en/documentation/installation/#rvm) or [rbenv](https://www.ruby-lang.org/en/documentation/installation/#rbenv) can help to manage the correct version for this automatically. + +See [official documentation](https://www.ruby-lang.org/en/documentation/installation/) for the most recent installation guidelines and available options. + +### Install devdocs + +Clone the repository. The first time you are at the `devdocs` directory, run: + +```bash +bundle install +``` + +The website file structure contains directories pulled from multiple sources, not only this repository. The full list with mapped directories is defined in the [Docfile.yml](./Docfile.yml). It includes public and private repositories. +To pull all the mapped sources: + +```bash +rake init +``` + +Docfile begins with public sources, because the `rake init` task fails when it attempts to clone content from private repositories without the corresponding permissions. + +>**NOTE** +>By default _rake_ clones using SSH. If you want to clone with HTTPS, you can run it with the `token` variable: +> +>```bash +>token=none rake init +>``` +> +> Use `none` if you do not want to use a real token. To have access to private repositories, you will need a [GitHub token](https://github.com/settings/tokens) with the relevant access permissions. + +>**TIP** +>All the helper CLI commands for this project are implemented using [rake](https://github.com/ruby/rake). +Use the `rake --tasks` command for a complete list of available tasks, or filter the list using a keyword, such as `rake --tasks test`. + +Once you have completed preparing your environment, you can build locally and preview the site in your browser. + +### Run the website + +1. Using the following rake task, verify all the required dependencies and start the embedded web server: + + ```bash + rake preview + ``` + + You will see the commands called by the rake task and the corresponding output. Each command is typically highlighted with the magenta color: + + ```terminal + ~/magento/devdocs (master)$ rake preview + Install gems listed in the Gemfile: $ bundle install + Using rake 13.0.1 + Using public_suffix 4.0.3 + + Bundle complete! 16 Gemfile dependencies, 70 gems now installed. + Use `bundle info [gemname]` to see where a bundled gem is installed. + Installed! + Cleaning after the last site generation: $ bundle exec jekyll clean + Configuration file: /Users/user/magento/devdocs/_config.yml + Cleaner: Removing /Users/user/magento/devdocs/_site... + Cleaner: Removing src/.jekyll-metadata... + Cleaner: Removing src/.jekyll-cache... + Cleaner: Nothing to do for .sass-cache. + Clean! + Enabled the default configuration: $ bundle exec jekyll serve --incremental \ + --open-url \ + --livereload \ + --trace \ + --plugins _plugins,_checks + Configuration file: /Users/user/magento/devdocs/_config.yml + Theme Config file: /Users/user/.rvm/gems/ruby-2.6.5/bundler/gems/devdocs-theme-e1a4ff6880d5/ _config.yml + Source: /Users/user/magento/devdocs/src + Destination: /Users/user/magento/devdocs/_site + Incremental build: enabled + Generating... + Running ["ImageCheck", "HtmlCheck", "LinkCheck", "ScriptCheck", "LinkChecker::DoubleSlashCheck"] on ["/Users/user/magento/devdocs/_site"] on *.html... + + + Ran on 1747 files! + + + HTML-Proofer finished successfully. + done in 220.316 seconds. + Auto-regeneration: enabled for 'src' + LiveReload address: http://127.0.0.1:35729 + Server address: http://127.0.0.1:4000/ + Server running... press ctrl-c to stop. + LiveReload: Browser connected + ``` + +1. The generated website launches automatically in a new tab in your browser. +1. Press `Ctrl+C` in the serve terminal to stop the server. + +> ***TIP*** +> Leave the serve terminal open and running. Every time you save changes to a file, it automatically regenerates the site so you can test the output immediately. Changing the `_config.yml` file or other YAML file with data or configuration requires a fresh build (stop and start the server again with `rake preview`). + +## Building old versions + +The published website contains documentation for the latest 2.3.x Adobe Commerce and Magento Open Source release only. For cases, when you need to view the content as it was for an earlier release, we created [tags](https://github.com/magento/devdocs/tags) in this repository. Typically, they point at the commit when the release notes were finalized and published. + +To view the list of available tags: + +```bash +git tag --list +``` + +To checkout the version (for example 2.2.0): + +```bash +git checkout 2.2.0 +``` + +Find guidelines for building the site locally in the checked out README. + +>**NOTE** +>There is no guarantee the site will be built, since it can have dependencies on the external resources that are not available anymore. + +## Archived docs + +To view the archived documentation, see . + +*** + +Our public channels: + +- [Slack](https://magentocommeng.slack.com/archives/CAN932A3H) ([Join us](https://opensource.magento.com/slack)) +- diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000000..41dea3436c8 --- /dev/null +++ b/Rakefile @@ -0,0 +1,118 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# frozen_string_literal: true + +# This file contains tasks with no namespace. +# All namespaced tasks are defined in the '../rakelib' directory. +# Each namespace is defined in a separate file. +# For example, 'preview:all' is defined in the '../rakelib/preview.rake' file. +# To see the list of tasks to use, run 'rake -T'. + +require 'html-proofer' +require 'kramdown' +require 'launchy' +require 'colorator' +require 'csv' +require 'rdoc' + +# Require helper methods from the 'lib' directory +Dir.glob('lib/**/*.rb') { |file| require_relative(file) } + +# Instantiate Docfile data for usage in tasks +@content_map = DocConfig.new.content_map + +desc "Same as 'rake', 'rake preview'" +task default: %w[preview] + +desc "Same as 'test:report'" +task test: %w[test:md test:report test:unused_images test:unused_includes] + +desc 'Preview the devdocs locally' +task preview: %w[install clean] do + puts 'Generating devdocs locally ... '.magenta + if File.exist?('_config.local.yml') + print 'enabled the additional configuration parameters from _config.local.yml: $ '.magenta + sh 'bundle exec jekyll serve --incremental \ + --open-url \ + --livereload \ + --trace \ + --config _config.yml,_config.local.yml \ + --plugins _plugins,_checks' + else + Rake::Task['preview:all'].invoke + end +end + +task :clean do + print 'Cleaning after the last site generation: $ '.magenta + sh 'bundle exec jekyll clean' + puts 'Clean!'.green +end + +task :install do + print 'Install gems listed in the Gemfile: $ '.magenta + sh 'bundle install' + puts 'Installed!'.green +end + +desc 'Build the entire website' +task build: %w[clean] do + print 'Building the site with Jekyll: $ '.magenta + sh 'bundle exec jekyll build --verbose --trace' + puts 'Built!'.green +end + +desc 'Pull docs from external repositories' +task init: %w[multirepo:init] + +desc 'Run checks (image optimization and Markdown style linting).' +task check: %w[check:image_optim check:mdl] + +desc 'Generate data for a news digest. Default timeframe is a week since today. For other period, use "since" argument: since="jul 4"' +task :whatsnew do + since = ENV['since'] + current_file = 'src/_data/whats-new.yml' + generated_file = 'tmp/whats-new.yml' + current_data = YAML.load_file current_file + last_update = current_data['updated'] + print 'Generating data for the What\'s New digest: $ '.magenta + + # Generate tmp/whats-new.yml + report = + if since.nil? || since.empty? + `bin/whatsup_github since '#{last_update}'` + elsif since.is_a? String + `bin/whatsup_github since '#{since}'` + else + abort 'The "since" argument must be a string. Example: "jul 4"' + end + + # Merge generated tmp/whats-new.yml with existing src/_data/whats-new.yml + generated_data = YAML.load_file generated_file + current_data['updated'] = generated_data['updated'] + current_data['entries'].prepend(generated_data['entries']).flatten! + current_data['entries'].uniq! { |entry| entry['link'] } + + puts "Writing updates to #{current_file}" + File.write current_file, current_data.to_yaml + + abort report if report.include? 'MISSING whatsnew' + puts report +end + +desc 'Generate index for Algolia' +task index: %w[init] do + puts 'Generating index for Algolia ...' + sh 'bin/jekyll', + 'algolia', + '--config=_config.yml,_config.index.yml' +end + +desc 'Convert HTML text to kramdown in your terminal' +task :convert do + puts 'Paste HTML text followed by a new line and press Control-D.'.magenta + result = `bin/kramdown --input=html --output=kramdown` + puts 'Converted text:'.magenta + puts result.bold +end diff --git a/_checks/html_check_hook.rb b/_checks/html_check_hook.rb new file mode 100644 index 00000000000..819d57a6d04 --- /dev/null +++ b/_checks/html_check_hook.rb @@ -0,0 +1,51 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# frozen_string_literal: true + +# The hook runs html-proofer with options defined in the +# _config.checks.yml file +# +# For more details about html-proofer, refer to: https://github.com/gjtorikian/html-proofer +# For more details about Jekyll hooks, refer to: https://jekyllrb.com/docs/plugins/hooks/ +# +require 'html-proofer' +require 'yaml' +require_relative '../lib/double_slash_check' + +Jekyll::Hooks.register :site, :post_write do |site| + # Do nothing unless 'site.check_links' is set + next unless site.config['check_links'] + + # Do not exit when html-proofer raises an error + begin + # Check 'ignore_urls' in '_config.checks.yml' + # and add 'excludes' from Jekyll configuration. + # + checks_config = YAML.load_file('_config.checks.yml') + ignore_urls = checks_config.dig('html-proofer', :ignore_urls) + jekyll_excludes = site.config['exclude'] + jekyll_excludes_as_regex = + jekyll_excludes.map do |item| + Regexp.new Regexp.escape(item) + end + + if ignore_urls + ignore_urls.push(jekyll_excludes_as_regex).flatten!.uniq! + else + checks_config['html-proofer'][:ignore_urls] = jekyll_excludes_as_regex + end + + # Read configuration options for html-proofer + options = checks_config['html-proofer'] + + # Run html-proofer to check the jekyll destination directory + HTMLProofer.check_directory(site.dest, options).run + + # Show the message when html-proofer fails. + # Expected that it fails when it finds broken links. + rescue StandardError => e + puts e + puts 'Fix the broken links before you push the changes to remote branch.'.blue + end +end diff --git a/_checks/styles/style-rules-dev b/_checks/styles/style-rules-dev new file mode 100644 index 00000000000..a5e85207409 --- /dev/null +++ b/_checks/styles/style-rules-dev @@ -0,0 +1,38 @@ +rule 'MD001' +exclude_rule 'MD002' +rule 'MD003', :style => :atx +rule 'MD004' +rule 'MD005' +rule 'MD006' +rule 'MD007', :indent => 3 +rule 'MD009' +rule 'MD010' +rule 'MD011' +rule 'MD012' +exclude_rule 'MD013' +exclude_rule 'MD014' +rule 'MD018' +rule 'MD019' +exclude_rule 'MD020' +exclude_rule 'MD021' +rule 'MD022' +rule 'MD023' +rule 'MD024', :allow_different_nesting => true +rule 'MD025' +exclude_rule 'MD026' +exclude_rule 'MD027' +exclude_rule 'MD028' +rule 'MD029' +rule 'MD030', :ul_single => 2, :ol_single => 1, :ul_multi => 2, :ol_multi => 1 +rule 'MD031' +rule 'MD032' +exclude_rule 'MD033' +exclude_rule 'MD034' +rule 'MD035' +rule 'MD036' +rule 'MD037' +rule 'MD038' +rule 'MD039' +rule 'MD040' +exclude_rule 'MD041' +rule 'MD046' diff --git a/_checks/styles/style-rules-prod b/_checks/styles/style-rules-prod new file mode 100644 index 00000000000..a5038295e51 --- /dev/null +++ b/_checks/styles/style-rules-prod @@ -0,0 +1,38 @@ +rule 'MD001' # Header levels should only increment by one level at a time +exclude_rule 'MD002' +rule 'MD003', :style => :atx # Header style +rule 'MD004' # Unordered list style; default "consistent" +rule 'MD005' # Inconsistent indentation for list items at the same level +rule 'MD006' # Consider starting bulleted lists at the beginning of the line +rule 'MD007', :indent => 3 # Unordered list indentation +rule 'MD009' # Trailing spaces; default: 0 +rule 'MD010' # Hard tabs +exclude_rule 'MD011' +rule 'MD012' # Multiple consecutive blank lines +exclude_rule 'MD013' +exclude_rule 'MD014' +rule 'MD018' # No space after hash on atx style header +rule 'MD019' # Multiple spaces after hash on atx style header +exclude_rule 'MD020' +exclude_rule 'MD021' +exclude_rule 'MD022' +rule 'MD023' # Headers must start at the beginning of the line +exclude_rule 'MD024' +rule 'MD025' # Multiple top level headers in the same document +exclude_rule 'MD026' +exclude_rule 'MD027' +exclude_rule 'MD028' +rule 'MD029' # Ordered list item prefix +rule 'MD030', :ul_single => 2, :ol_single => 1, :ul_multi => 2, :ol_multi => 1 # Spaces after list markers +exclude_rule 'MD031' +rule 'MD032' # Lists should be surrounded by blank lines +exclude_rule 'MD033' +exclude_rule 'MD034' +rule 'MD035' # Horizontal rule style; default "consistent" +rule 'MD036' # Emphasis used instead of a header +rule 'MD037' # Spaces inside emphasis markers +rule 'MD038' # Spaces inside code span elements +exclude_rule 'MD039' +rule 'MD040' # Fenced code blocks should have a language specified +exclude_rule 'MD041' +rule 'MD046' diff --git a/_config.checks.yml b/_config.checks.yml new file mode 100644 index 00000000000..789f1526c03 --- /dev/null +++ b/_config.checks.yml @@ -0,0 +1,25 @@ +# Configuration settings for html-proofer. +# For configuration documentation, refer to: https://github.com/gjtorikian/html-proofer#configuration +# For configuration defaults, refer to: https://github.com/gjtorikian/html-proofer/blob/master/lib/html-proofer/configuration.rb +# +html-proofer: + + # Do not flag a tags missing href + :allow_missing_href: true + + # Do not check external links. + :disable_external: true + + # Ignore images with missing alt tags + :ignore_missing_alt: true + + # Ignores images with empty alt tags. + :ignore_empty_alt: true + + # Ignore entirely the files which pathname matches a specified pattern + :ignore_files: + - !ruby/regexp /guides\/v2\.3\/mrg/ + - !ruby/regexp /guides\/v2\.4\/mrg/ + - !ruby/regexp /mbi/ + - !ruby/regexp /mftf/ + - !ruby/regexp /page-builder/ diff --git a/_config.index.yml b/_config.index.yml new file mode 100644 index 00000000000..691fe0a9000 --- /dev/null +++ b/_config.index.yml @@ -0,0 +1,22 @@ +# Jekyll Algolia settings for indexing https://community.algolia.com/jekyll-algolia/options.html +algolia: + files_to_exclude: + - swagger + - redoc + # Index HTML elements, especially code samples. + nodes_to_index: 'p,li,td,code' + # Override 10KB default and use our allowed max of 20KB + max_record_size: 20000 + settings: + attributesForFaceting: + - searchable(functional_areas) + - searchable(guide_version) + - searchable(versionless) + +# Disable the code blocks line numbers at indexing stage +kramdown: + syntax_highlighter_opts: + span: + line_numbers: false + block: + line_numbers: false diff --git a/_config.prod.yml b/_config.prod.yml new file mode 100644 index 00000000000..42da3160c62 --- /dev/null +++ b/_config.prod.yml @@ -0,0 +1,2 @@ +# Set the site environment for the tag manager +environment: public diff --git a/_config.stage.yml b/_config.stage.yml new file mode 100644 index 00000000000..4c25d9eead8 --- /dev/null +++ b/_config.stage.yml @@ -0,0 +1 @@ +url: "https://devdocs.magedevteam.com" \ No newline at end of file diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000000..f6df5bc28c5 --- /dev/null +++ b/_config.yml @@ -0,0 +1,237 @@ +# This file contains configuration settings for the devdocs website. +# Each setting is available on pages using Liquid {{ site. }}. For example, {{ site.title }}. +# For more details about available options and defaults, refer to https://jekyllrb.com/docs/configuration/ +# +title: Adobe Commerce Developer Guide +logo: Commerce DevDocs +description: Adobe Commerce Developer Guide + +# the base protocol and domain +url: https://devdocs.magento.com + +# Site theme: https://github.com/commerce-docs/devdocs-theme +# About themes in Jekyll: https://jekyllrb.com/docs/themes/ +theme: devdocs + +# A part of URL to be inserted between site.url and page.url +baseurl: "" + +source: src + +plugins_dir: ./_plugins + +# Parameters for the version switcher. Numeric values must be wrapped in quotes. +# +version: "2.4" +versions: + - + name: "2.3" + url: /guides/v2.3/ + - + name: "2.4" + url: /guides/v2.4/ + + +collections: + videos: + output: true + permalink: "/videos/:path/" + +# Specific settings for different scopes. +# +defaults: + + - + scope: + path: "" + values: + lang: en + layout: default + github_repo: https://github.com/magento/devdocs/ + github_files: https://github.com/magento/devdocs/blob/master/ + # Enables the 'Edit this page on GitHub' appearances on pages + github_link: false + # Enables the 'Give us feedback' appearances on pages + feedback_link: false + + - + scope: + path: guides/v2.3 + values: + guide_version: '2.3' + + - + scope: + path: guides/v2.4 + values: + guide_version: '2.4' + + - + scope: + type: videos + values: + layout: video + github_link: false + feedback_link: false + + - + scope: + path: mbi + values: + group: mbi + github_files: https://github.com/magento/devdocs-mbi/blob/master/ + github_repo: https://github.com/magento/devdocs-mbi/ + + - + scope: + path: mftf/docs + values: + group: mftf + github_files: https://github.com/magento/magento2-functional-testing-framework/blob/master/ + github_repo: https://github.com/magento/magento2-functional-testing-framework/ + functional_areas: + - Test + + - + scope: + path: mftf/v2 + values: + guide_version: '2.3' + group: mftf-v2 + github_files: https://github.com/magento/magento2-functional-testing-framework/tree/2.6.5/ + github_repo: https://github.com/magento/magento2-functional-testing-framework/ + functional_areas: + - Test + - + scope: + path: page-builder + values: + group: page-builder + github_files: https://github.com/magento/magento2-page-builder-docs/tree/master/ + github_repo: https://github.com/magento/magento2-page-builder-docs/ + + - + scope: + path: magento-payments + values: + group: magento-payments + + - + scope: + path: guides/v2.3/mrg + values: + group: module-reference-guide-2_3 + github_link: false + + - + scope: + path: guides/v2.4/mrg + values: + group: module-reference-guide-2_4 + layout: migrated + + - + scope: + path: guides/v2.3/install-gde + values: + group: installation-guide + + - + scope: + path: guides/v2.4/install-gde + values: + group: installation-guide + + +########################## +# Plugins and extensions # +########################## +# +# For more details about plugins, refer to https://jekyllrb.com/docs/plugins/ +# To learn more about particular plugin, find it by name on GitHub. +# This list doesn't include custom plugins. +# +plugins: + - jekyll-optional-front-matter + - jekyll-redirect-from + - jekyll-relative-links + - jekyll-sitemap + - jekyll-titles-from-headings + +optional_front_matter: + remove_originals: true + +# Configuration options for the jekyll-relative-links plugin. +# Exclude all .md directories and files except page-builder/ to prevent painfully long build times. + +relative_links: + enabled: true + collections: false + exclude: + - 404.md + - codelinks + - community + - extensions + - guides + - redoc + - release + - search + - system-requirements.md + - whats-new.md + +# Settings for the jekyll-titles-from-headings plugin. +# For more details about the plugin, refer to https://github.com/benbalter/jekyll-titles-from-headings +# +titles_from_headings: + enabled: true + strip_title: true + collections: false + +# kramdown parser settings (extended Markdown to HTML parser). For more details about available options, refer to https://kramdown.gettalong.org/converter/html.html +# +kramdown: + toc_levels: 2..3 + syntax_highlighter_opts: + css_class: highlighter + span: + line_numbers: false + block: + line_numbers: true + +# Search engine settings. For more details, refer to https://github.com/algolia/jekyll-algolia +# +algolia: + application_id: E642SEDTHL + index_name: devdocs + # search-only API key allows to search data immediately. + # It is safe to use in production front-end code. + # Used at src/_includes/layout/header-scripts.html + # For more details, refer to: https://www.algolia.com/doc/guides/security/api-keys/#search-only-api-key + search_only_key: d2d0f33ab73e291ef8d88d8b565e754c #gitleaks:allow + +google: + gtm: GTM-KRCLXBB + +# Toggles the _plugin/html-check-hook.rb script that checks links in the generated HTML +# +check_links: true + +############# +# Variables # +############# +# +# Magento 2 repository +mage2bloburl: https://github.com/magento/magento2/blob + +# Current version +gdeurl: /guides/v2.4 + +# URL to downloadable binary files, such as .ai, .psd, .zip, .pdf, .sketch +downloads: https://devdocs.magento.com/download + +# URL to the User guide +user_guide_url: https://docs.magento.com/user-guide + +# Patterns to exclude for Jekyll +exclude: + - .git diff --git a/_includes/footer.html b/_includes/footer.html deleted file mode 100644 index 8a03fbd5617..00000000000 --- a/_includes/footer.html +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/_includes/header.html b/_includes/header.html deleted file mode 100644 index add8abc3480..00000000000 --- a/_includes/header.html +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - {% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %} - - - - - - - - - - - - - - -
- - - - - - -
- -
-
- - - - diff --git a/_includes/navigation.html b/_includes/navigation.html deleted file mode 100644 index 03776a507ea..00000000000 --- a/_includes/navigation.html +++ /dev/null @@ -1,54 +0,0 @@ - - diff --git a/_includes/page-header.html b/_includes/page-header.html deleted file mode 100644 index 24bdc7fc971..00000000000 --- a/_includes/page-header.html +++ /dev/null @@ -1,9 +0,0 @@ - -
-
-

{{ page.title }}

- {% if page.github_link %} - Edit this page on GitHub - {% endif %} -
-
\ No newline at end of file diff --git a/_includes/sidebar.html b/_includes/sidebar.html deleted file mode 100644 index ff462ba6914..00000000000 --- a/_includes/sidebar.html +++ /dev/null @@ -1,68 +0,0 @@ - - - \ No newline at end of file diff --git a/_layouts/default.html b/_layouts/default.html deleted file mode 100644 index 9fca7a642e5..00000000000 --- a/_layouts/default.html +++ /dev/null @@ -1,19 +0,0 @@ -{% include header.html %} - -{% include page-header.html %} - -
-
-
- - {{ content }} - -
-
- - -{% include sidebar.html %} - -
- -{% include footer.html %} \ No newline at end of file diff --git a/_layouts/home.html b/_layouts/home.html deleted file mode 100644 index 21f36c784d4..00000000000 --- a/_layouts/home.html +++ /dev/null @@ -1,5 +0,0 @@ -{% include header.html %} - -{{ content }} - -{% include footer.html %} \ No newline at end of file diff --git a/_plugins/algolia.rb b/_plugins/algolia.rb new file mode 100644 index 00000000000..d33d86583ba --- /dev/null +++ b/_plugins/algolia.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +module Jekyll + module Algolia + module Hooks + def self.before_indexing_each(record, _node, _context) + # Do not index records larger than 20000 bytes + return nil if record.to_s.bytesize > 20_000 + + record + end + end + end +end diff --git a/_plugins/debug/site_post_render.rb b/_plugins/debug/site_post_render.rb new file mode 100644 index 00000000000..8e72a2bae95 --- /dev/null +++ b/_plugins/debug/site_post_render.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# To enable this plugin, add to your '_config.local.yml' the following: +# +# debug: site_post_render +# +# This plugin runs an IRB session (https://github.com/ruby/irb) of a Jekyll application in a serving mode when it's at a state after rendering the whole site, but before writing any files. +# See the ":site, :post_render" hook: https://jekyllrb.com/docs/plugins/hooks/ +# Available objects to explore are 'site' and 'payload'. +# +# Several helpful methods (to use a method, chain to an object such as 'site.methods', 'payload.keys'): +# - '.methods.sort' +# - '.instance_variables.sort' +# - '.keys.sort' +# +# Examples: +# +# To view available configuration data of the site +# > payload.site.keys +# +# To view the number of pages: +# > payload.site.pages.count +# +# To find a page by path and view its data: +# > page = payload.site.pages.select { |page| page.path == 'cloud/env/variables-build.md' }[0] +# > page.data +# +# To exit from the IRB session: +# > exit! +# +Jekyll::Hooks.register :site, :post_render do |site, payload| + next unless site.config['serving'] + + # rubocop:disable Lint/Debugger + binding.irb if site.config['debug'] == 'site_post_render' + # rubocop:enable Lint/Debugger +end diff --git a/_plugins/debug/site_pre_render.rb b/_plugins/debug/site_pre_render.rb new file mode 100644 index 00000000000..bae552f6b0c --- /dev/null +++ b/_plugins/debug/site_pre_render.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# To enable this plugin, add to your '_config.local.yml' the following: +# +# debug: site_pre_render +# +# This plugin runs an IRB session (https://github.com/ruby/irb) of a Jekyll application in a serving mode when it's at a state just before rendering the whole site. +# See the ":site, :pre_render" hook: https://jekyllrb.com/docs/plugins/hooks/ +# Available objects to explore are 'site' and 'payload'. +# +# Several helpful methods (to use a method, chain to an object such as 'site.methods', 'payload.keys'): +# - '.methods.sort' +# - '.instance_variables.sort' +# - '.keys.sort' +# +# Examples: +# +# To view available configuration data of the site +# > payload.site.keys +# +# To view the number of pages: +# > payload.site.pages.count +# +# To find a page by path and view its data: +# > page = payload.site.pages.select { |page| page.path == 'cloud/env/variables-build.md' }[0] +# > page.data +# +# To exit from the IRB session: +# > exit! +# +Jekyll::Hooks.register :site, :pre_render do |site, payload| + next unless site.config['serving'] + + # rubocop:disable Lint/Debugger + binding.irb if site.config['debug'] == 'site_pre_render' + # rubocop:enable Lint/Debugger +end diff --git a/_plugins/generators/migrated_log.rb b/_plugins/generators/migrated_log.rb new file mode 100644 index 00000000000..0177d92729a --- /dev/null +++ b/_plugins/generators/migrated_log.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +# Copyright © Adobe, Inc. All rights reserved. +# See COPYING.txt for license details. + +# This plugin generates the page that contains a list of migrated topics: https://devdocs.magento.com/migrated.html +# It adds global data: +# - site.data.migration.migrated_pages +# - site.data.migration.deprecated_pages +# - site.data.migration.all_migrating_pages +# - site.data.migration.remained_migrating_pages +# +# And generates the `tmp/migrated-from-to.csv` file with the list of links "from" and "to" for the migrated pages. +# To enable the file generation, add 'migrated_log: generate_file' to _config.local.yml. +# + +module Jekyll + # Custom generator for MRG pages + class MigratedLog < Generator + safe true + + def generate(site) + @site = site + pages = @site.pages + migrated_pages = pages.select { |page| page.data['status']&.include? 'migrated' } + v2_3_pages = pages.select { |page| page.data['guide_version'] == '2.3' } + remained_pages = pages - v2_3_pages + deprecated_pages = remained_pages.select { |page| page.data['group'].nil? || (page.data['redirect_to'] && !page.data['status']) } + all_migrating_pages = remained_pages - deprecated_pages + remained_migrating_pages = all_migrating_pages - migrated_pages + migrated_pages_data = [] + + if (site.config['migrated_log']&.include? 'generate_file') + # Create a CSV file that contains links 'from' and 'to' for migrated pages + migrated_pages = pages.select { |pages| pages.data['status']&.include? 'migrated' } + redirects = migrated_pages.map { |page| "https://devdocs.magento.com#{page.data['redirect']['from']},#{page.data['redirect']['to']}" } + File.write('tmp/migrated-from-to.csv', redirects.join("\n")) + end + + # Create an array of JSON objects that contain metadata for migrated pages + migrated_pages.each do |page| + migrated_page = { + path: page.path, + title: page.data['title'] || abort("Error in '#{page.path}'.\n Check 'title' in the file's frontmatter.".red), + guide: if page.data['layout'].include?('video') + 'Video Tutorials' + else + @site.data.dig('toc', page.data['group'], + 'label') || abort("Error in '#{page.path}'.\n Check 'group' in the file's frontmatter or 'label' in the corresponding TOC.".red) + end, + migrated_from: site.baseurl + page.url, + redirected_to: page.data['redirect_to'] || abort("Error in '#{page.path}'.\n Check 'redirect_to' in the file's frontmatter.".red), + redirected_to_source: if page.data['redirect_to'].start_with?('https://experienceleague.adobe.com') + 'Adobe Experience League' + elsif page.data['redirect_to'].start_with?('https://developer.adobe.com') + 'Adobe Developer' + else + abort "Error in '#{page.path}'.\nThe 'redirected_to' parameter in the front matter points to the wrong domain: #{page.data['redirect_to']}.\nShould be 'https://experienceleague.adobe.com' or 'https://developer.adobe.com'".red + end + } + migrated_pages_data << migrated_page + end + + # Group migrated pages by guide + migrated_pages_by_group = migrated_pages_data.group_by { |page| page[:guide] }.sort.to_h + # Introductory text in the Migrated topics page + content = "The following #{migrated_pages.size} topics have been migrated and redirected.\n\n" + migrated_pages_by_group.each do |guide, topics| + content += "\n## #{guide}\n\n\n" + topics.sort_by { |topic| topic[:title] } + .each do |topic| + content += "1. [#{topic[:title]}](#{topic[:migrated_from]}) has moved to [#{topic[:redirected_to_source]}](#{topic[:redirected_to]})\n" + end + end + + content += "\n***\n\n\n" + content += "\n## Pages to be migrated\n\n\n" + + if remained_migrating_pages.empty? + content += 'All 2.4 and versionless pages were migrated' + else + remained_migrating_pages.sort_by(&:path) + .each do |page| + content += "1. `#{page.path}`\n" + end + end + + # PageWithoutAFile handles processing files without reading it. + # 'migrated.md' is a virtual file that's been created during Jekyll run. + # See details in https://www.rubydoc.info/gems/jekyll/Jekyll/PageWithoutAFile + # See tests in https://github.com/jekyll/jekyll/blob/master/test/test_page_without_a_file.rb + topic = PageWithoutAFile.new( + @site, + @site.source, + '.', + 'migrated.md' + ) + topic.content = content + topic.data['title'] = 'Migrated topics' + topic.data['layout'] = 'full-width' + topic.data['github_link'] = false + topic.data['feedback_link'] = false + topic.process('migrated.md') + + # Add the newly constructed page object to the rest of pages + # on the site. + pages << topic + + site.data['migration'] = + { + 'migrated_pages' => migrated_pages.map(&:path), + 'deprecated_pages' => deprecated_pages.map(&:path), + 'all_migrating_pages' => all_migrating_pages.map(&:path), + 'remained_migrating_pages' => remained_migrating_pages.map(&:path) + } + + migrated_pages_data + end + end +end diff --git a/_plugins/liquid-tags/collapsible.rb b/_plugins/liquid-tags/collapsible.rb new file mode 100644 index 00000000000..beb4b415ae3 --- /dev/null +++ b/_plugins/liquid-tags/collapsible.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# Author: jcalcaben@magento.com +# +# This custom plugin adds a block tag that wraps the content in +# a jquery-ui accordion widget. +# +module Jekyll + # Collapsibility for regular blocks + class Collapsible < Liquid::Block + def initialize(tag_name, title, tokens) + super + @title = title.to_s + end + + def render(context) + site = context.registers[:site] + + converter = if defined? site.find_converter_instance + site.find_converter_instance(Jekyll::Converters::Markdown) + else + site.getConverterImpl(::Jekyll::Converters::Markdown) + end + + content = super.strip + content = converter.convert(content) + <<-HTML +
+ #{@title} +
#{content}
+
+ HTML + end + end + + ## TODO: Come up with cleaner solution + # Collapsibility as headings of level 2 + class CollapsibleH2 < Liquid::Block + def initialize(tag_name, title, tokens) + super + @title = title.to_s + end + + def render(context) + site = context.registers[:site] + + converter = if defined? site.find_converter_instance + site.find_converter_instance(Jekyll::Converters::Markdown) + else + site.getConverterImpl(::Jekyll::Converters::Markdown) + end + + content = super.strip + content = converter.convert(content) + <<-HTML +
+

#{@title}

+
#{content}
+
+ HTML + end + end + + # Collapsibility as headings of level 3 + class CollapsibleH3 < Liquid::Block + def initialize(tag_name, title, tokens) + super + @title = title.to_s + end + + def render(context) + site = context.registers[:site] + + converter = if defined? site.find_converter_instance + site.find_converter_instance(Jekyll::Converters::Markdown) + else + site.getConverterImpl(::Jekyll::Converters::Markdown) + end + + content = super.strip + content = converter.convert(content) + <<-HTML +
+

#{@title}

+
#{content}
+
+ HTML + end + end +end + +Liquid::Template.register_tag('collapsible', Jekyll::Collapsible) +Liquid::Template.register_tag('collapsibleh2', Jekyll::CollapsibleH2) +Liquid::Template.register_tag('collapsibleh3', Jekyll::CollapsibleH3) diff --git a/_plugins/page-params/github_path.rb b/_plugins/page-params/github_path.rb new file mode 100644 index 00000000000..368d0274f50 --- /dev/null +++ b/_plugins/page-params/github_path.rb @@ -0,0 +1,32 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# frozen_string_literal: true + +# +# This custom plugin dynamically sets the 'github_path' parameter +# for each page except 'redirect.html' pages. +# A value of the parameter is available as {{ page.github_path }}. +# The parameter contains a file path relative to its repository. +# +Jekyll::Hooks.register :pages, :post_init do |page| + # Skip virtual pages like MRG topics + next if page.is_a? Jekyll::PageWithoutAFile + # Process only files with 'md' and 'html' extensions + next unless File.extname(page.path).match?(/md|html/) + # Skip redirects + next if page.name == 'redirect.html' + + dir = File.join( + page.site.source, + File.dirname(page.path) + ) + + filename = File.basename page.path + + # Change to the parent directory of the page and read full file path + # from git index. + Dir.chdir(dir) do + page.data['github_path'] = `git ls-files --full-name #{filename}`.strip + end +end diff --git a/_plugins/page-params/guide_version.rb b/_plugins/page-params/guide_version.rb new file mode 100644 index 00000000000..7be1f9cafe3 --- /dev/null +++ b/_plugins/page-params/guide_version.rb @@ -0,0 +1,24 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# frozen_string_literal: true + +# This plugin allows to fix an indexing issue, when 'guide_version' parameter +# does not appear in Algolia. +# For all topics within '/guides/v<...>/', the 'guide_version' parameter +# is explicitly added to 'page.data' from 'defaults' in '_config.yml'. +# +Jekyll::Hooks.register :pages, :pre_render do |page| + # Process only files with 'md' and 'html' extensions + next unless File.extname(page.path).match?(/md|html/) + + # Do nothing for redirects + next if page.name == 'redirect.html' + + # Process only pages that have URL starting with '/guides/v' + filtering_pattern = '/guides/v' + + next unless page.url.start_with? filtering_pattern + + page.data['guide_version'] = page.instance_variable_get('@defaults')['guide_version'] +end diff --git a/_plugins/page-params/last_modified_at.rb b/_plugins/page-params/last_modified_at.rb new file mode 100644 index 00000000000..ac35554760c --- /dev/null +++ b/_plugins/page-params/last_modified_at.rb @@ -0,0 +1,45 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# frozen_string_literal: true + +# +# This custom plugin dynamically sets the 'last_modified_at' parameter +# for each page except 'redirect.html' pages. +# A value of the parameter is available as {{ page.last_modified_at }}. +# The parameter contains date and time of the last commit that changed +# the original file. +# For available date formats, refer to https://git-scm.com/docs/git-log#git-log---dateltformatgt +# +Jekyll::Hooks.register :pages, :post_init do |page| + # Do nothing in serving mode + next if page.site.config['serving'] + + # Process only files with 'md' and 'html' extensions + next unless File.extname(page.path).match?(/md|html/) + + # Skip redirects + next if page.name == 'redirect.html' + + # Skip pages where the parameter is already set + next if page.data['last_modified_at'] + + # Skip pages created by custom generators like 'mrg_pages' + next if page.is_a? Jekyll::PageWithoutAFile + + # Add site.source to the page path + file_path = File.join(page.site.source, page.path) + + # Get real path of the page. If this is a symlink read it to get path of the real file with content. + real_filepath = File.realpath(file_path) + + # Get a full path of the directory where the page is stored + dir = File.dirname(real_filepath) + + # Change directory to the parent directory of the page to read from the corresponding git history. + Dir.chdir(dir) do + # Read date of the last commit and assign it to last_modified_at parameter + # of the page. + page.data['last_modified_at'] = `git log -1 --format=%cd --date=iso -- #{page.name}`.strip + end +end diff --git a/_plugins/page-params/page_baseurl_generator.rb b/_plugins/page-params/page_baseurl_generator.rb new file mode 100644 index 00000000000..5c7766d4778 --- /dev/null +++ b/_plugins/page-params/page_baseurl_generator.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# Author: jcalcaben@magento.com +# +# This custom plugin dynamically sets and injects the page.baseurl variable +# based on the page's destination. +# +# The hook introduces the page.baseurl parameter for each page. +# For pages at guides/v2.x the page.baseurl parameter is set +# as "{site.baseurl}/guides/v#{version}". +# The {version} is taken from the 'guide_version' front matter parameter on the page; +# if it is not set, then the 'guide_version' is set to version from the _config.yml depending on a scope by path (for example, "2.4" at the "guides/v2.4/"); +# otherwise, the version is unset and returns the nil object (same as null). +Jekyll::Hooks.register :pages, :post_init do |page| + # Glossary. Create and assign variables to be used in the script. + baseurl = page.site.baseurl + data = page.data + version = data['guide_version'] + + # Create 'page.baseurl' parameter + data['baseurl'] = + if version + "#{baseurl}/guides/v#{version}" + else + baseurl + end +end diff --git a/_plugins/page-params/page_versionless.rb b/_plugins/page-params/page_versionless.rb new file mode 100644 index 00000000000..289e526c172 --- /dev/null +++ b/_plugins/page-params/page_versionless.rb @@ -0,0 +1,22 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# frozen_string_literal: true + +# +# For all topics outside '/guides/v<...>/', the 'page.versionless' parameter is set to true. +# +Jekyll::Hooks.register :pages, :post_init do |page| + # Process only files with 'md' and 'html' extensions + next unless File.extname(page.path).match?(/md|html/) + + # Do nothing for redirects + next if page.name == 'redirect.html' + + # Process only pages that have URL starting with '/guides/v' + filtering_pattern = '/guides/v' + + next if page.url.start_with? filtering_pattern + + page.data['versionless'] = true +end diff --git a/_plugins/page-params/page_versions.rb b/_plugins/page-params/page_versions.rb new file mode 100644 index 00000000000..f26958103ac --- /dev/null +++ b/_plugins/page-params/page_versions.rb @@ -0,0 +1,68 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +# frozen_string_literal: true + +# +# This custom plugin dynamically sets the 'versions' parameter +# for a page at any '/guides/v<...>/' directory. +# The parameter is available as a liquid expression {{ page.versions }}. +# It stores an array of hashes like [ { 'name' => '2.1', 'url' = guides/v2.1/index.html }, { 'name' => '2.2', 'url' = guides/v2.2/index.html }, etc ]. +# The parameter contains all available versions of the topic. +# +# +Jekyll::Hooks.register :pages, :pre_render do |page, config| + # Process only files with 'md' and 'html' extensions + next page unless File.extname(page.path).match?(/md|html/) + + # Do nothing for redirects + next page if page.name == 'redirect.html' + + # Process only pages that have URL starting with '/guides/v' + filtering_pattern = '/guides/v' + + # Get all page objects at the site + pages = page.site.pages + + # Select pages that do not have name 'redirect.html' and their URL + # starts with '/guides/v'. Get 'url' of each page and store them as an array. + urls_filtered_by_pattern = + pages.select do |site_page| + next if site_page.name == 'redirect.html' + + site_page.url.start_with? filtering_pattern + end.map(&:url) + + url = page.url + + # Get the nonversion path from URL removing prefix 'guides/v<...>/'. + versioned_prefix_pattern = %r{\A#{filtering_pattern}[^/]+} + non_version_path = url.sub(versioned_prefix_pattern, '') + + # Define a regular expression to match all versions of a topic + full_path_pattern = /#{versioned_prefix_pattern}#{non_version_path}\Z/ + + # Get URLs for all versions of the topic + versioned_urls = + urls_filtered_by_pattern.select { |path| path.match full_path_pattern } + + # Define a regular expression to get a version number from URL + # to the 'version_from_path' variable + version_pattern = %r{\A#{filtering_pattern}(?[^/]+)} + + # Get all versioned URLs of the topic into array of key-value pairs + # like { 'name' => '2.0', 'url' => '/guides/v2.3/index.html' } + versions = + versioned_urls.map do |v_url| + version_pattern.match(v_url) + version = Regexp.last_match(:version_from_path) + { + 'name' => version, + 'url' => v_url + } + end + + # Set the page.versions parameter to sorted array of key-value pairs + # from 'versions' + config['page']['versions'] = versions.sort_by { |version| version['name'] } +end diff --git a/_plugins/page-params/redirect_23.rb b/_plugins/page-params/redirect_23.rb new file mode 100644 index 00000000000..4636ab5e805 --- /dev/null +++ b/_plugins/page-params/redirect_23.rb @@ -0,0 +1,46 @@ +# Copyright 2023 Adobe +# All Rights Reserved. +# +# NOTICE: All information contained herein is, and remains +# the property of Adobe and its suppliers, if any. The intellectual +# and technical concepts contained herein are proprietary to Adobe +# and its suppliers and are protected by all applicable intellectual +# property laws, including trade secret and copyright laws. +# Dissemination of this information or reproduction of this material +# is strictly forbidden + +# This plugin redirects 2.3 pages to the DevSite. +# It uses redirect metadata from the 2.4 version of the page. +# If there is no 2.4 version of the page, then it redirects to https://developer.adobe.com/commerce/docs/ + +# frozen_string_literal: true + +Jekyll::Hooks.register :site, :post_read do |site| + pages = site.pages + + pages.each do |page| + # Skip pages where the parameter is already set + next unless page.path.start_with? 'guides/v2.3/' + + # Process only files with 'md' and 'html' extensions + next unless File.extname(page.path).match?(/md|html/) + + # Skip redirects + next if page.name == 'redirect.html' + + # Skip pages where the parameter is already set + next if page.data['redirect_to'] + + path_23 = page.path + + path_24 = path_23.sub('v2.3', 'v2.4') + + page_24 = pages.find { |page| page.path == path_24 } + + if page_24.nil? + page.data['redirect_to'] = 'https://developer.adobe.com/commerce/docs/' + else + page.data['redirect_to'] = page_24.data['redirect_to'] + end + end +end diff --git a/_plugins/page-params/redirect_mftf_v2.rb b/_plugins/page-params/redirect_mftf_v2.rb new file mode 100644 index 00000000000..7427ea2d931 --- /dev/null +++ b/_plugins/page-params/redirect_mftf_v2.rb @@ -0,0 +1,28 @@ +# Copyright 2023 Adobe +# All Rights Reserved. +# +# NOTICE: All information contained herein is, and remains +# the property of Adobe and its suppliers, if any. The intellectual +# and technical concepts contained herein are proprietary to Adobe +# and its suppliers and are protected by all applicable intellectual +# property laws, including trade secret and copyright laws. +# Dissemination of this information or reproduction of this material +# is strictly forbidden + +# This plugin redirects MFTF v2 pages to the MFTF repo. + +# frozen_string_literal: true + +Jekyll::Hooks.register :site, :post_read do |site| + pages = site.pages + + pages.each do |page| + # Skip pages where the parameter is already set + next unless page.path.start_with? 'mftf/v2' + + # Process only files with 'md' and 'html' extensions + next unless File.extname(page.path).match?(/md/) + + page.data['redirect_to'] = "https://github.com/magento/magento2-functional-testing-framework/blob/2.x-develop/#{page.path.delete_prefix('mftf/v2/')}" + end +end diff --git a/_plugins/page.rb b/_plugins/page.rb deleted file mode 100644 index 1e8a22a0579..00000000000 --- a/_plugins/page.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Jekyll - - # Extensions to the Jekyll Page class. - - class Page - - # Full URL of the page. - def full_url - File.join(self.url) - end - - - end -end \ No newline at end of file diff --git a/_plugins/site.rb b/_plugins/site.rb deleted file mode 100644 index 28b5323ba16..00000000000 --- a/_plugins/site.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Jekyll - - # Extensions to the Jekyll Site class. - - class Site - - # Regular expression by which blog posts are recognized - GUIDE_PAGE_RE = /\/guides/ - - # Find my blog posts among all the pages. - def guides - self.pages.select {|p| p.full_url =~ GUIDE_PAGE_RE} - end - - - # Add some custom options to the site payload, accessible via the - # "site" variable within templates. - # - # articles - blog articles, in reverse chronological order - # max_recent - maximum number of recent articles to display - alias orig_site_payload site_payload - def site_payload - h = orig_site_payload - payload = h["site"] - payload["articles"] = guides - #payload["max_recent"] = payload.fetch("max_recent", 15) - h["site"] = payload - h - end - - end -end \ No newline at end of file diff --git a/bin/htmlproofer b/bin/htmlproofer new file mode 100755 index 00000000000..b7e0c6b17ed --- /dev/null +++ b/bin/htmlproofer @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'htmlproofer' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('html-proofer', 'htmlproofer') diff --git a/bin/image_optim b/bin/image_optim new file mode 100755 index 00000000000..10eeb949276 --- /dev/null +++ b/bin/image_optim @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'image_optim' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('image_optim', 'image_optim') diff --git a/bin/jekyll b/bin/jekyll new file mode 100755 index 00000000000..86ee6e5fccc --- /dev/null +++ b/bin/jekyll @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'jekyll' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', + Pathname.new(__FILE__).realpath) + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('jekyll', 'jekyll') diff --git a/bin/kramdown b/bin/kramdown new file mode 100755 index 00000000000..79836a090b6 --- /dev/null +++ b/bin/kramdown @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'kramdown' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('kramdown', 'kramdown') diff --git a/bin/mdl b/bin/mdl new file mode 100755 index 00000000000..eedf2b4756f --- /dev/null +++ b/bin/mdl @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'mdl' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('mdl', 'mdl') diff --git a/bin/whatsup_github b/bin/whatsup_github new file mode 100755 index 00000000000..de9ee9e1e22 --- /dev/null +++ b/bin/whatsup_github @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'whatsup_github' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path('bundle', __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('whatsup_github', 'whatsup_github') diff --git a/common/css/bootstrap-theme.css b/common/css/bootstrap-theme.css deleted file mode 100755 index f860bbc069d..00000000000 --- a/common/css/bootstrap-theme.css +++ /dev/null @@ -1,442 +0,0 @@ -/*! - * Bootstrap v3.2.0 (http://getbootstrap.com) - * Copyright 2011-2014 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -.btn-default, -.btn-primary, -.btn-success, -.btn-info, -.btn-warning, -.btn-danger { - text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); -} -.btn-default:active, -.btn-primary:active, -.btn-success:active, -.btn-info:active, -.btn-warning:active, -.btn-danger:active, -.btn-default.active, -.btn-primary.active, -.btn-success.active, -.btn-info.active, -.btn-warning.active, -.btn-danger.active { - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); -} -.btn:active, -.btn.active { - background-image: none; -} -.btn-default { - text-shadow: 0 1px 0 #fff; - background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); - background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); - background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #dbdbdb; - border-color: #ccc; -} -.btn-default:hover, -.btn-default:focus { - background-color: #e0e0e0; - background-position: 0 -15px; -} -.btn-default:active, -.btn-default.active { - background-color: #e0e0e0; - border-color: #dbdbdb; -} -.btn-default:disabled, -.btn-default[disabled] { - background-color: #e0e0e0; - background-image: none; -} -.btn-primary { - background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); - background-image: -o-linear-gradient(top, #428bca 0%, #2d6ca2 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#2d6ca2)); - background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #2b669a; -} -.btn-primary:hover, -.btn-primary:focus { - background-color: #2d6ca2; - background-position: 0 -15px; -} -.btn-primary:active, -.btn-primary.active { - background-color: #2d6ca2; - border-color: #2b669a; -} -.btn-primary:disabled, -.btn-primary[disabled] { - background-color: #2d6ca2; - background-image: none; -} -.btn-success { - background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); - background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); - background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #3e8f3e; -} -.btn-success:hover, -.btn-success:focus { - background-color: #419641; - background-position: 0 -15px; -} -.btn-success:active, -.btn-success.active { - background-color: #419641; - border-color: #3e8f3e; -} -.btn-success:disabled, -.btn-success[disabled] { - background-color: #419641; - background-image: none; -} -.btn-info { - background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); - background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); - background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #28a4c9; -} -.btn-info:hover, -.btn-info:focus { - background-color: #2aabd2; - background-position: 0 -15px; -} -.btn-info:active, -.btn-info.active { - background-color: #2aabd2; - border-color: #28a4c9; -} -.btn-info:disabled, -.btn-info[disabled] { - background-color: #2aabd2; - background-image: none; -} -.btn-warning { - background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); - background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); - background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #e38d13; -} -.btn-warning:hover, -.btn-warning:focus { - background-color: #eb9316; - background-position: 0 -15px; -} -.btn-warning:active, -.btn-warning.active { - background-color: #eb9316; - border-color: #e38d13; -} -.btn-warning:disabled, -.btn-warning[disabled] { - background-color: #eb9316; - background-image: none; -} -.btn-danger { - background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); - background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); - background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #b92c28; -} -.btn-danger:hover, -.btn-danger:focus { - background-color: #c12e2a; - background-position: 0 -15px; -} -.btn-danger:active, -.btn-danger.active { - background-color: #c12e2a; - border-color: #b92c28; -} -.btn-danger:disabled, -.btn-danger[disabled] { - background-color: #c12e2a; - background-image: none; -} -.thumbnail, -.img-thumbnail { - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); - box-shadow: 0 1px 2px rgba(0, 0, 0, .075); -} -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - background-color: #e8e8e8; - background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); - background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); - background-repeat: repeat-x; -} -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - background-color: #357ebd; - background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: -o-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd)); - background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); - background-repeat: repeat-x; -} -.navbar-default { - background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); - background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); - background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); -} -.navbar-default .navbar-nav > .active > a { - background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); - background-image: -o-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f3f3f3)); - background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); - background-repeat: repeat-x; - -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); - box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); -} -.navbar-brand, -.navbar-nav > li > a { - text-shadow: 0 1px 0 rgba(255, 255, 255, .25); -} -.navbar-inverse { - background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); - background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); - background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; -} -.navbar-inverse .navbar-nav > .active > a { - background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%); - background-image: -o-linear-gradient(top, #222 0%, #282828 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#222), to(#282828)); - background-image: linear-gradient(to bottom, #222 0%, #282828 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); - background-repeat: repeat-x; - -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); - box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); -} -.navbar-inverse .navbar-brand, -.navbar-inverse .navbar-nav > li > a { - text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); -} -.navbar-static-top, -.navbar-fixed-top, -.navbar-fixed-bottom { - border-radius: 0; -} -.alert { - text-shadow: 0 1px 0 rgba(255, 255, 255, .2); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); -} -.alert-success { - background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); - background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); - background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); - background-repeat: repeat-x; - border-color: #b2dba1; -} -.alert-info { - background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); - background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); - background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); - background-repeat: repeat-x; - border-color: #9acfea; -} -.alert-warning { - background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); - background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); - background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); - background-repeat: repeat-x; - border-color: #f5e79e; -} -.alert-danger { - background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); - background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); - background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); - background-repeat: repeat-x; - border-color: #dca7a7; -} -.progress { - background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); - background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); - background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar { - background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); - background-image: -o-linear-gradient(top, #428bca 0%, #3071a9 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3071a9)); - background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-success { - background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); - background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); - background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-info { - background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); - background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); - background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-warning { - background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); - background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); - background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-danger { - background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); - background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); - background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-striped { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.list-group { - border-radius: 4px; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); - box-shadow: 0 1px 2px rgba(0, 0, 0, .075); -} -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - text-shadow: 0 -1px 0 #3071a9; - background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); - background-image: -o-linear-gradient(top, #428bca 0%, #3278b3 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3278b3)); - background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); - background-repeat: repeat-x; - border-color: #3278b3; -} -.panel { - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); - box-shadow: 0 1px 2px rgba(0, 0, 0, .05); -} -.panel-default > .panel-heading { - background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); - background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); - background-repeat: repeat-x; -} -.panel-primary > .panel-heading { - background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: -o-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd)); - background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); - background-repeat: repeat-x; -} -.panel-success > .panel-heading { - background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); - background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); - background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); - background-repeat: repeat-x; -} -.panel-info > .panel-heading { - background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); - background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); - background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); - background-repeat: repeat-x; -} -.panel-warning > .panel-heading { - background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); - background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); - background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); - background-repeat: repeat-x; -} -.panel-danger > .panel-heading { - background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); - background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); - background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); - background-repeat: repeat-x; -} -.well { - background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); - background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); - background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); - background-repeat: repeat-x; - border-color: #dcdcdc; - -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); -} -/*# sourceMappingURL=bootstrap-theme.css.map */ diff --git a/common/css/bootstrap-theme.css.map b/common/css/bootstrap-theme.css.map deleted file mode 100755 index 4cc41ab001a..00000000000 --- a/common/css/bootstrap-theme.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"bootstrap-theme.css","sources":["less/theme.less","less/mixins/vendor-prefixes.less","bootstrap-theme.css","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":"AAeA;;;;;;EAME,0CAAA;EC+CA,6FAAA;EACQ,qFAAA;EC5DT;AFiBC;;;;;;;;;;;;EC0CA,0DAAA;EACQ,kDAAA;EC7CT;AFqCC;;EAEE,wBAAA;EEnCH;AFwCD;EG/CI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EA+B2C,2BAAA;EAA2B,oBAAA;EE7BvE;AFAC;;EAEE,2BAAA;EACA,8BAAA;EEEH;AFCC;;EAEE,2BAAA;EACA,uBAAA;EECH;AFEC;;EAEE,2BAAA;EACA,wBAAA;EEAH;AFeD;EGhDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EE0BD;AFxBC;;EAEE,2BAAA;EACA,8BAAA;EE0BH;AFvBC;;EAEE,2BAAA;EACA,uBAAA;EEyBH;AFtBC;;EAEE,2BAAA;EACA,wBAAA;EEwBH;AFRD;EGjDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EEkDD;AFhDC;;EAEE,2BAAA;EACA,8BAAA;EEkDH;AF/CC;;EAEE,2BAAA;EACA,uBAAA;EEiDH;AF9CC;;EAEE,2BAAA;EACA,wBAAA;EEgDH;AF/BD;EGlDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EE0ED;AFxEC;;EAEE,2BAAA;EACA,8BAAA;EE0EH;AFvEC;;EAEE,2BAAA;EACA,uBAAA;EEyEH;AFtEC;;EAEE,2BAAA;EACA,wBAAA;EEwEH;AFtDD;EGnDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EEkGD;AFhGC;;EAEE,2BAAA;EACA,8BAAA;EEkGH;AF/FC;;EAEE,2BAAA;EACA,uBAAA;EEiGH;AF9FC;;EAEE,2BAAA;EACA,wBAAA;EEgGH;AF7ED;EGpDI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EAEA,wHAAA;ECnBF,qEAAA;EJ8BA,6BAAA;EACA,uBAAA;EE0HD;AFxHC;;EAEE,2BAAA;EACA,8BAAA;EE0HH;AFvHC;;EAEE,2BAAA;EACA,uBAAA;EEyHH;AFtHC;;EAEE,2BAAA;EACA,wBAAA;EEwHH;AF7FD;;ECbE,oDAAA;EACQ,4CAAA;EC8GT;AFvFD;;EGvEI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EHsEF,2BAAA;EE6FD;AF3FD;;;EG5EI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH4EF,2BAAA;EEiGD;AFvFD;EG1FI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ECnBF,qEAAA;EJ4GA,oBAAA;EC9CA,6FAAA;EACQ,qFAAA;EC4IT;AFlGD;EG1FI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EF2CF,0DAAA;EACQ,kDAAA;ECqJT;AF/FD;;EAEE,gDAAA;EEiGD;AF7FD;EG5GI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ECnBF,qEAAA;EFgOD;AFrGD;EG5GI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EF2CF,yDAAA;EACQ,iDAAA;EC0KT;AF9GD;;EAWI,2CAAA;EEuGH;AFlGD;;;EAGE,kBAAA;EEoGD;AF1FD;EACE,+CAAA;EC3FA,4FAAA;EACQ,oFAAA;ECwLT;AFlFD;EGtJI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH8IF,uBAAA;EE8FD;AFzFD;EGvJI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH8IF,uBAAA;EEsGD;AFhGD;EGxJI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH8IF,uBAAA;EE8GD;AFvGD;EGzJI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EH8IF,uBAAA;EEsHD;AFtGD;EGlKI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED2QH;AFnGD;EG5KI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDkRH;AFzGD;EG7KI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDyRH;AF/GD;EG9KI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDgSH;AFrHD;EG/KI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDuSH;AF3HD;EGhLI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED8SH;AF9HD;EGnJI,+MAAA;EACA,0MAAA;EACA,uMAAA;EDoRH;AF1HD;EACE,oBAAA;EC/IA,oDAAA;EACQ,4CAAA;EC4QT;AF3HD;;;EAGE,+BAAA;EGpME,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EHkMF,uBAAA;EEiID;AFvHD;ECjKE,mDAAA;EACQ,2CAAA;EC2RT;AFjHD;EG1NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED8UH;AFvHD;EG3NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDqVH;AF7HD;EG5NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED4VH;AFnID;EG7NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDmWH;AFzID;EG9NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;ED0WH;AF/ID;EG/NI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EDiXH;AF9ID;EGvOI,0EAAA;EACA,qEAAA;EACA,+FAAA;EAAA,wEAAA;EACA,6BAAA;EACA,wHAAA;EHqOF,uBAAA;EC1LA,2FAAA;EACQ,mFAAA;EC+UT","sourcesContent":["\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &:disabled,\n &[disabled] {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-bg, 5%); @end-color: darken(@navbar-default-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-bg; @end-color: lighten(@navbar-inverse-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n}\n\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They will be removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n &::-moz-placeholder { color: @color; // Firefox\n opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n",null,"// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} \ No newline at end of file diff --git a/common/css/bootstrap-theme.min.css b/common/css/bootstrap-theme.min.css deleted file mode 100755 index 2e97597c876..00000000000 --- a/common/css/bootstrap-theme.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Bootstrap v3.2.0 (http://getbootstrap.com) - * Copyright 2011-2014 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-o-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#2d6ca2));background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary:disabled,.btn-primary[disabled]{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f3f3f3));background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:-o-linear-gradient(top,#222 0,#282828 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#222),to(#282828));background-image:linear-gradient(to bottom,#222 0,#282828 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-o-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3071a9));background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-o-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3278b3));background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);background-repeat:repeat-x;border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} \ No newline at end of file diff --git a/common/css/bootstrap.css b/common/css/bootstrap.css deleted file mode 100755 index a9f35ceedfa..00000000000 --- a/common/css/bootstrap.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Bootstrap v3.2.0 (http://getbootstrap.com) - * Copyright 2011-2014 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - *//*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;width:100% \9;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;width:100% \9;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}mark,.mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#777;opacity:1}.form-control:-ms-input-placeholder{color:#777}.form-control::-webkit-input-placeholder{color:#777}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px;line-height:1.42857143 \0}input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:20px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],input[type=radio].disabled,input[type=checkbox].disabled,fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm,.form-horizontal .form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg,.form-horizontal .form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:25px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#3071a9;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{position:absolute;z-index:-1;filter:alpha(opacity=0);opacity:0}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#777}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#777}.navbar-inverse .navbar-nav>li>a{color:#777}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#777}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#777}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#428bca;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar[aria-valuenow="1"],.progress-bar[aria-valuenow="2"]{min-width:30px}.progress-bar[aria-valuenow="0"]{min-width:30px;color:#777;background-color:transparent;background-image:none;-webkit-box-shadow:none;box-shadow:none}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#777;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#428bca}.panel-primary>.panel-heading .badge{color:#428bca;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate3d(0,-25%,0);-o-transform:translate3d(0,-25%,0);transform:translate3d(0,-25%,0)}.modal.in .modal-dialog{-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-size:12px;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed;-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/common/css/bootstrap.css.map b/common/css/bootstrap.css.map deleted file mode 100755 index bfb5616891b..00000000000 --- a/common/css/bootstrap.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"bootstrap.css","sources":["bootstrap.css","less/normalize.less","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"AAAA,6DAA4D;ACQ5D;EACE,yBAAA;EACA,4BAAA;EACA,gCAAA;EDND;ACaD;EACE,WAAA;EDXD;ACuBD;;;;;;;;;;;;EAYE,gBAAA;EDrBD;AC6BD;;;;EAIE,uBAAA;EACA,0BAAA;ED3BD;ACmCD;EACE,eAAA;EACA,WAAA;EDjCD;ACyCD;;EAEE,eAAA;EDvCD;ACiDD;EACE,yBAAA;ED/CD;ACsDD;;EAEE,YAAA;EDpDD;AC8DD;EACE,2BAAA;ED5DD;ACmED;;EAEE,mBAAA;EDjED;ACwED;EACE,oBAAA;EDtED;AC8ED;EACE,gBAAA;EACA,kBAAA;ED5ED;ACmFD;EACE,kBAAA;EACA,aAAA;EDjFD;ACwFD;EACE,gBAAA;EDtFD;AC6FD;;EAEE,gBAAA;EACA,gBAAA;EACA,oBAAA;EACA,0BAAA;ED3FD;AC8FD;EACE,aAAA;ED5FD;AC+FD;EACE,iBAAA;ED7FD;ACuGD;EACE,WAAA;EDrGD;AC4GD;EACE,kBAAA;ED1GD;ACoHD;EACE,kBAAA;EDlHD;ACyHD;EACE,8BAAA;EACA,iCAAA;EAAA,yBAAA;EACA,WAAA;EDvHD;AC8HD;EACE,gBAAA;ED5HD;ACmID;;;;EAIE,mCAAA;EACA,gBAAA;EDjID;ACmJD;;;;;EAKE,gBAAA;EACA,eAAA;EACA,WAAA;EDjJD;ACwJD;EACE,mBAAA;EDtJD;ACgKD;;EAEE,sBAAA;ED9JD;ACyKD;;;;EAIE,4BAAA;EACA,iBAAA;EDvKD;AC8KD;;EAEE,iBAAA;ED5KD;ACmLD;;EAEE,WAAA;EACA,YAAA;EDjLD;ACyLD;EACE,qBAAA;EDvLD;ACkMD;;EAEE,gCAAA;EAAA,6BAAA;EAAA,wBAAA;EACA,YAAA;EDhMD;ACyMD;;EAEE,cAAA;EDvMD;ACgND;EACE,+BAAA;EACA,8BAAA;EACA,iCAAA;EACA,yBAAA;ED9MD;ACuND;;EAEE,0BAAA;EDrND;AC4ND;EACE,2BAAA;EACA,eAAA;EACA,gCAAA;ED1ND;ACkOD;EACE,WAAA;EACA,YAAA;EDhOD;ACuOD;EACE,gBAAA;EDrOD;AC6OD;EACE,mBAAA;ED3OD;ACqPD;EACE,2BAAA;EACA,mBAAA;EDnPD;ACsPD;;EAEE,YAAA;EDpPD;AE9ED;EA9FE;IACE,8BAAA;IACA,wBAAA;IACA,oCAAA;IACA,qCAAA;IAAA,6BAAA;IF+KD;EE5KD;;IAEE,4BAAA;IF8KD;EE3KD;IACE,8BAAA;IF6KD;EE1KD;IACE,+BAAA;IF4KD;EExKD;;IAEE,aAAA;IF0KD;EEvKD;;IAEE,wBAAA;IACA,0BAAA;IFyKD;EEtKD;IACE,6BAAA;IFwKD;EErKD;;IAEE,0BAAA;IFuKD;EEpKD;IACE,4BAAA;IFsKD;EEnKD;;;IAGE,YAAA;IACA,WAAA;IFqKD;EElKD;;IAEE,yBAAA;IFoKD;EE/JD;IACE,6BAAA;IFiKD;EE7JD;IACE,eAAA;IF+JD;EE7JD;;IAGI,mCAAA;IF8JH;EE3JD;;IAGI,mCAAA;IF4JH;EEzJD;IACE,wBAAA;IF2JD;EExJD;IACE,sCAAA;IF0JD;EExJD;;IAGI,mCAAA;IFyJH;EACF;AGhPD;EACE,qCAAA;EACA,uDAAA;EACA,6TAAA;EHkPD;AG3OD;EACE,oBAAA;EACA,UAAA;EACA,uBAAA;EACA,qCAAA;EACA,oBAAA;EACA,qBAAA;EACA,gBAAA;EACA,qCAAA;EACA,oCAAA;EH6OD;AGzOmC;EAAW,gBAAA;EH4O9C;AG3OmC;EAAW,gBAAA;EH8O9C;AG7OmC;EAAW,kBAAA;EHgP9C;AG/OmC;EAAW,kBAAA;EHkP9C;AGjPmC;EAAW,kBAAA;EHoP9C;AGnPmC;EAAW,kBAAA;EHsP9C;AGrPmC;EAAW,kBAAA;EHwP9C;AGvPmC;EAAW,kBAAA;EH0P9C;AGzPmC;EAAW,kBAAA;EH4P9C;AG3PmC;EAAW,kBAAA;EH8P9C;AG7PmC;EAAW,kBAAA;EHgQ9C;AG/PmC;EAAW,kBAAA;EHkQ9C;AGjQmC;EAAW,kBAAA;EHoQ9C;AGnQmC;EAAW,kBAAA;EHsQ9C;AGrQmC;EAAW,kBAAA;EHwQ9C;AGvQmC;EAAW,kBAAA;EH0Q9C;AGzQmC;EAAW,kBAAA;EH4Q9C;AG3QmC;EAAW,kBAAA;EH8Q9C;AG7QmC;EAAW,kBAAA;EHgR9C;AG/QmC;EAAW,kBAAA;EHkR9C;AGjRmC;EAAW,kBAAA;EHoR9C;AGnRmC;EAAW,kBAAA;EHsR9C;AGrRmC;EAAW,kBAAA;EHwR9C;AGvRmC;EAAW,kBAAA;EH0R9C;AGzRmC;EAAW,kBAAA;EH4R9C;AG3RmC;EAAW,kBAAA;EH8R9C;AG7RmC;EAAW,kBAAA;EHgS9C;AG/RmC;EAAW,kBAAA;EHkS9C;AGjSmC;EAAW,kBAAA;EHoS9C;AGnSmC;EAAW,kBAAA;EHsS9C;AGrSmC;EAAW,kBAAA;EHwS9C;AGvSmC;EAAW,kBAAA;EH0S9C;AGzSmC;EAAW,kBAAA;EH4S9C;AG3SmC;EAAW,kBAAA;EH8S9C;AG7SmC;EAAW,kBAAA;EHgT9C;AG/SmC;EAAW,kBAAA;EHkT9C;AGjTmC;EAAW,kBAAA;EHoT9C;AGnTmC;EAAW,kBAAA;EHsT9C;AGrTmC;EAAW,kBAAA;EHwT9C;AGvTmC;EAAW,kBAAA;EH0T9C;AGzTmC;EAAW,kBAAA;EH4T9C;AG3TmC;EAAW,kBAAA;EH8T9C;AG7TmC;EAAW,kBAAA;EHgU9C;AG/TmC;EAAW,kBAAA;EHkU9C;AGjUmC;EAAW,kBAAA;EHoU9C;AGnUmC;EAAW,kBAAA;EHsU9C;AGrUmC;EAAW,kBAAA;EHwU9C;AGvUmC;EAAW,kBAAA;EH0U9C;AGzUmC;EAAW,kBAAA;EH4U9C;AG3UmC;EAAW,kBAAA;EH8U9C;AG7UmC;EAAW,kBAAA;EHgV9C;AG/UmC;EAAW,kBAAA;EHkV9C;AGjVmC;EAAW,kBAAA;EHoV9C;AGnVmC;EAAW,kBAAA;EHsV9C;AGrVmC;EAAW,kBAAA;EHwV9C;AGvVmC;EAAW,kBAAA;EH0V9C;AGzVmC;EAAW,kBAAA;EH4V9C;AG3VmC;EAAW,kBAAA;EH8V9C;AG7VmC;EAAW,kBAAA;EHgW9C;AG/VmC;EAAW,kBAAA;EHkW9C;AGjWmC;EAAW,kBAAA;EHoW9C;AGnWmC;EAAW,kBAAA;EHsW9C;AGrWmC;EAAW,kBAAA;EHwW9C;AGvWmC;EAAW,kBAAA;EH0W9C;AGzWmC;EAAW,kBAAA;EH4W9C;AG3WmC;EAAW,kBAAA;EH8W9C;AG7WmC;EAAW,kBAAA;EHgX9C;AG/WmC;EAAW,kBAAA;EHkX9C;AGjXmC;EAAW,kBAAA;EHoX9C;AGnXmC;EAAW,kBAAA;EHsX9C;AGrXmC;EAAW,kBAAA;EHwX9C;AGvXmC;EAAW,kBAAA;EH0X9C;AGzXmC;EAAW,kBAAA;EH4X9C;AG3XmC;EAAW,kBAAA;EH8X9C;AG7XmC;EAAW,kBAAA;EHgY9C;AG/XmC;EAAW,kBAAA;EHkY9C;AGjYmC;EAAW,kBAAA;EHoY9C;AGnYmC;EAAW,kBAAA;EHsY9C;AGrYmC;EAAW,kBAAA;EHwY9C;AGvYmC;EAAW,kBAAA;EH0Y9C;AGzYmC;EAAW,kBAAA;EH4Y9C;AG3YmC;EAAW,kBAAA;EH8Y9C;AG7YmC;EAAW,kBAAA;EHgZ9C;AG/YmC;EAAW,kBAAA;EHkZ9C;AGjZmC;EAAW,kBAAA;EHoZ9C;AGnZmC;EAAW,kBAAA;EHsZ9C;AGrZmC;EAAW,kBAAA;EHwZ9C;AGvZmC;EAAW,kBAAA;EH0Z9C;AGzZmC;EAAW,kBAAA;EH4Z9C;AG3ZmC;EAAW,kBAAA;EH8Z9C;AG7ZmC;EAAW,kBAAA;EHga9C;AG/ZmC;EAAW,kBAAA;EHka9C;AGjamC;EAAW,kBAAA;EHoa9C;AGnamC;EAAW,kBAAA;EHsa9C;AGramC;EAAW,kBAAA;EHwa9C;AGvamC;EAAW,kBAAA;EH0a9C;AGzamC;EAAW,kBAAA;EH4a9C;AG3amC;EAAW,kBAAA;EH8a9C;AG7amC;EAAW,kBAAA;EHgb9C;AG/amC;EAAW,kBAAA;EHkb9C;AGjbmC;EAAW,kBAAA;EHob9C;AGnbmC;EAAW,kBAAA;EHsb9C;AGrbmC;EAAW,kBAAA;EHwb9C;AGvbmC;EAAW,kBAAA;EH0b9C;AGzbmC;EAAW,kBAAA;EH4b9C;AG3bmC;EAAW,kBAAA;EH8b9C;AG7bmC;EAAW,kBAAA;EHgc9C;AG/bmC;EAAW,kBAAA;EHkc9C;AGjcmC;EAAW,kBAAA;EHoc9C;AGncmC;EAAW,kBAAA;EHsc9C;AGrcmC;EAAW,kBAAA;EHwc9C;AGvcmC;EAAW,kBAAA;EH0c9C;AGzcmC;EAAW,kBAAA;EH4c9C;AG3cmC;EAAW,kBAAA;EH8c9C;AG7cmC;EAAW,kBAAA;EHgd9C;AG/cmC;EAAW,kBAAA;EHkd9C;AGjdmC;EAAW,kBAAA;EHod9C;AGndmC;EAAW,kBAAA;EHsd9C;AGrdmC;EAAW,kBAAA;EHwd9C;AGvdmC;EAAW,kBAAA;EH0d9C;AGzdmC;EAAW,kBAAA;EH4d9C;AG3dmC;EAAW,kBAAA;EH8d9C;AG7dmC;EAAW,kBAAA;EHge9C;AG/dmC;EAAW,kBAAA;EHke9C;AGjemC;EAAW,kBAAA;EHoe9C;AGnemC;EAAW,kBAAA;EHse9C;AGremC;EAAW,kBAAA;EHwe9C;AGvemC;EAAW,kBAAA;EH0e9C;AGzemC;EAAW,kBAAA;EH4e9C;AG3emC;EAAW,kBAAA;EH8e9C;AG7emC;EAAW,kBAAA;EHgf9C;AG/emC;EAAW,kBAAA;EHkf9C;AGjfmC;EAAW,kBAAA;EHof9C;AGnfmC;EAAW,kBAAA;EHsf9C;AGrfmC;EAAW,kBAAA;EHwf9C;AGvfmC;EAAW,kBAAA;EH0f9C;AGzfmC;EAAW,kBAAA;EH4f9C;AG3fmC;EAAW,kBAAA;EH8f9C;AG7fmC;EAAW,kBAAA;EHggB9C;AG/fmC;EAAW,kBAAA;EHkgB9C;AGjgBmC;EAAW,kBAAA;EHogB9C;AGngBmC;EAAW,kBAAA;EHsgB9C;AGrgBmC;EAAW,kBAAA;EHwgB9C;AGvgBmC;EAAW,kBAAA;EH0gB9C;AGzgBmC;EAAW,kBAAA;EH4gB9C;AG3gBmC;EAAW,kBAAA;EH8gB9C;AG7gBmC;EAAW,kBAAA;EHghB9C;AG/gBmC;EAAW,kBAAA;EHkhB9C;AGjhBmC;EAAW,kBAAA;EHohB9C;AGnhBmC;EAAW,kBAAA;EHshB9C;AGrhBmC;EAAW,kBAAA;EHwhB9C;AGvhBmC;EAAW,kBAAA;EH0hB9C;AGzhBmC;EAAW,kBAAA;EH4hB9C;AG3hBmC;EAAW,kBAAA;EH8hB9C;AG7hBmC;EAAW,kBAAA;EHgiB9C;AG/hBmC;EAAW,kBAAA;EHkiB9C;AGjiBmC;EAAW,kBAAA;EHoiB9C;AGniBmC;EAAW,kBAAA;EHsiB9C;AGriBmC;EAAW,kBAAA;EHwiB9C;AGviBmC;EAAW,kBAAA;EH0iB9C;AGziBmC;EAAW,kBAAA;EH4iB9C;AG3iBmC;EAAW,kBAAA;EH8iB9C;AG7iBmC;EAAW,kBAAA;EHgjB9C;AG/iBmC;EAAW,kBAAA;EHkjB9C;AGjjBmC;EAAW,kBAAA;EHojB9C;AGnjBmC;EAAW,kBAAA;EHsjB9C;AGrjBmC;EAAW,kBAAA;EHwjB9C;AGvjBmC;EAAW,kBAAA;EH0jB9C;AGzjBmC;EAAW,kBAAA;EH4jB9C;AG3jBmC;EAAW,kBAAA;EH8jB9C;AG7jBmC;EAAW,kBAAA;EHgkB9C;AG/jBmC;EAAW,kBAAA;EHkkB9C;AGjkBmC;EAAW,kBAAA;EHokB9C;AGnkBmC;EAAW,kBAAA;EHskB9C;AGrkBmC;EAAW,kBAAA;EHwkB9C;AGvkBmC;EAAW,kBAAA;EH0kB9C;AGzkBmC;EAAW,kBAAA;EH4kB9C;AG3kBmC;EAAW,kBAAA;EH8kB9C;AG7kBmC;EAAW,kBAAA;EHglB9C;AG/kBmC;EAAW,kBAAA;EHklB9C;AGjlBmC;EAAW,kBAAA;EHolB9C;AGnlBmC;EAAW,kBAAA;EHslB9C;AGrlBmC;EAAW,kBAAA;EHwlB9C;AGvlBmC;EAAW,kBAAA;EH0lB9C;AGzlBmC;EAAW,kBAAA;EH4lB9C;AG3lBmC;EAAW,kBAAA;EH8lB9C;AG7lBmC;EAAW,kBAAA;EHgmB9C;AG/lBmC;EAAW,kBAAA;EHkmB9C;AGjmBmC;EAAW,kBAAA;EHomB9C;AGnmBmC;EAAW,kBAAA;EHsmB9C;AGrmBmC;EAAW,kBAAA;EHwmB9C;AGvmBmC;EAAW,kBAAA;EH0mB9C;AGzmBmC;EAAW,kBAAA;EH4mB9C;AG3mBmC;EAAW,kBAAA;EH8mB9C;AG7mBmC;EAAW,kBAAA;EHgnB9C;AG/mBmC;EAAW,kBAAA;EHknB9C;AGjnBmC;EAAW,kBAAA;EHonB9C;AGnnBmC;EAAW,kBAAA;EHsnB9C;AGrnBmC;EAAW,kBAAA;EHwnB9C;AGvnBmC;EAAW,kBAAA;EH0nB9C;AIx1BD;ECgEE,gCAAA;EACG,6BAAA;EACK,wBAAA;EL2xBT;AI11BD;;EC6DE,gCAAA;EACG,6BAAA;EACK,wBAAA;ELiyBT;AIx1BD;EACE,iBAAA;EACA,+CAAA;EJ01BD;AIv1BD;EACE,6DAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,2BAAA;EJy1BD;AIr1BD;;;;EAIE,sBAAA;EACA,oBAAA;EACA,sBAAA;EJu1BD;AIj1BD;EACE,gBAAA;EACA,uBAAA;EJm1BD;AIj1BC;;EAEE,gBAAA;EACA,4BAAA;EJm1BH;AIh1BC;EErDA,sBAAA;EAEA,4CAAA;EACA,sBAAA;ENu4BD;AI10BD;EACE,WAAA;EJ40BD;AIt0BD;EACE,wBAAA;EJw0BD;AIp0BD;;;;;EGvEE,gBAAA;EACA,gBAAA;EACA,iBAAA;EACA,cAAA;EPk5BD;AIz0BD;EACE,oBAAA;EJ20BD;AIr0BD;EACE,cAAA;EACA,yBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;EC0FA,0CAAA;EACK,qCAAA;EACG,kCAAA;EEpLR,uBAAA;EACA,gBAAA;EACA,iBAAA;EACA,cAAA;EPm6BD;AIt0BD;EACE,oBAAA;EJw0BD;AIl0BD;EACE,kBAAA;EACA,qBAAA;EACA,WAAA;EACA,+BAAA;EJo0BD;AI5zBD;EACE,oBAAA;EACA,YAAA;EACA,aAAA;EACA,cAAA;EACA,YAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EJ8zBD;AItzBC;;EAEE,kBAAA;EACA,aAAA;EACA,cAAA;EACA,WAAA;EACA,mBAAA;EACA,YAAA;EJwzBH;AQn8BD;;;;;;;;;;;;EAEE,sBAAA;EACA,kBAAA;EACA,kBAAA;EACA,gBAAA;ER+8BD;AQp9BD;;;;;;;;;;;;;;;;;;;;;;;;EASI,qBAAA;EACA,gBAAA;EACA,gBAAA;ERq+BH;AQj+BD;;;;;;EAGE,kBAAA;EACA,qBAAA;ERs+BD;AQ1+BD;;;;;;;;;;;;EAQI,gBAAA;ERg/BH;AQ7+BD;;;;;;EAGE,kBAAA;EACA,qBAAA;ERk/BD;AQt/BD;;;;;;;;;;;;EAQI,gBAAA;ER4/BH;AQx/BD;;EAAU,iBAAA;ER4/BT;AQ3/BD;;EAAU,iBAAA;ER+/BT;AQ9/BD;;EAAU,iBAAA;ERkgCT;AQjgCD;;EAAU,iBAAA;ERqgCT;AQpgCD;;EAAU,iBAAA;ERwgCT;AQvgCD;;EAAU,iBAAA;ER2gCT;AQrgCD;EACE,kBAAA;ERugCD;AQpgCD;EACE,qBAAA;EACA,iBAAA;EACA,kBAAA;EACA,kBAAA;ERsgCD;AQjgCD;EAAA;IAFI,iBAAA;IRugCD;EACF;AQ//BD;;EAEE,gBAAA;ERigCD;AQ7/BD;EACE,oBAAA;ER+/BD;AQ5/BD;;EAEE,2BAAA;EACA,eAAA;ER8/BD;AQ1/BD;EAAuB,kBAAA;ER6/BtB;AQ5/BD;EAAuB,mBAAA;ER+/BtB;AQ9/BD;EAAuB,oBAAA;ERigCtB;AQhgCD;EAAuB,qBAAA;ERmgCtB;AQlgCD;EAAuB,qBAAA;ERqgCtB;AQlgCD;EAAuB,2BAAA;ERqgCtB;AQpgCD;EAAuB,2BAAA;ERugCtB;AQtgCD;EAAuB,4BAAA;ERygCtB;AQtgCD;EACE,gBAAA;ERwgCD;AQtgCD;EC1GE,gBAAA;ETmnCD;ASlnCC;EACE,gBAAA;ETonCH;AQzgCD;EC7GE,gBAAA;ETynCD;ASxnCC;EACE,gBAAA;ET0nCH;AQ5gCD;EChHE,gBAAA;ET+nCD;AS9nCC;EACE,gBAAA;ETgoCH;AQ/gCD;ECnHE,gBAAA;ETqoCD;ASpoCC;EACE,gBAAA;ETsoCH;AQlhCD;ECtHE,gBAAA;ET2oCD;AS1oCC;EACE,gBAAA;ET4oCH;AQjhCD;EAGE,aAAA;EEhIA,2BAAA;EVkpCD;AUjpCC;EACE,2BAAA;EVmpCH;AQlhCD;EEnIE,2BAAA;EVwpCD;AUvpCC;EACE,2BAAA;EVypCH;AQrhCD;EEtIE,2BAAA;EV8pCD;AU7pCC;EACE,2BAAA;EV+pCH;AQxhCD;EEzIE,2BAAA;EVoqCD;AUnqCC;EACE,2BAAA;EVqqCH;AQ3hCD;EE5IE,2BAAA;EV0qCD;AUzqCC;EACE,2BAAA;EV2qCH;AQzhCD;EACE,qBAAA;EACA,qBAAA;EACA,kCAAA;ER2hCD;AQnhCD;;EAEE,eAAA;EACA,qBAAA;ERqhCD;AQxhCD;;;;EAMI,kBAAA;ERwhCH;AQjhCD;EACE,iBAAA;EACA,kBAAA;ERmhCD;AQ/gCD;EALE,iBAAA;EACA,kBAAA;EAMA,mBAAA;ERkhCD;AQphCD;EAKI,uBAAA;EACA,mBAAA;EACA,oBAAA;ERkhCH;AQ7gCD;EACE,eAAA;EACA,qBAAA;ER+gCD;AQ7gCD;;EAEE,yBAAA;ER+gCD;AQ7gCD;EACE,mBAAA;ER+gCD;AQ7gCD;EACE,gBAAA;ER+gCD;AQt/BD;EAAA;IAVM,aAAA;IACA,cAAA;IACA,aAAA;IACA,mBAAA;IG3NJ,kBAAA;IACA,yBAAA;IACA,qBAAA;IXguCC;EQhgCH;IAHM,oBAAA;IRsgCH;EACF;AQ7/BD;;EAGE,cAAA;EACA,mCAAA;ER8/BD;AQ5/BD;EACE,gBAAA;EACA,2BAAA;ER8/BD;AQ1/BD;EACE,oBAAA;EACA,kBAAA;EACA,mBAAA;EACA,gCAAA;ER4/BD;AQv/BG;;;EACE,kBAAA;ER2/BL;AQrgCD;;;EAmBI,gBAAA;EACA,gBAAA;EACA,yBAAA;EACA,gBAAA;ERu/BH;AQr/BG;;;EACE,wBAAA;ERy/BL;AQj/BD;;EAEE,qBAAA;EACA,iBAAA;EACA,iCAAA;EACA,gBAAA;EACA,mBAAA;ERm/BD;AQ7+BG;;;;;;EAAW,aAAA;ERq/Bd;AQp/BG;;;;;;EACE,wBAAA;ER2/BL;AQr/BD;;EAEE,aAAA;ERu/BD;AQn/BD;EACE,qBAAA;EACA,oBAAA;EACA,yBAAA;ERq/BD;AYtyCD;;;;EAIE,gEAAA;EZwyCD;AYpyCD;EACE,kBAAA;EACA,gBAAA;EACA,gBAAA;EACA,2BAAA;EACA,oBAAA;EZsyCD;AYlyCD;EACE,kBAAA;EACA,gBAAA;EACA,gBAAA;EACA,2BAAA;EACA,oBAAA;EACA,wDAAA;EAAA,gDAAA;EZoyCD;AY1yCD;EASI,YAAA;EACA,iBAAA;EACA,0BAAA;EAAA,kBAAA;EZoyCH;AY/xCD;EACE,gBAAA;EACA,gBAAA;EACA,kBAAA;EACA,iBAAA;EACA,yBAAA;EACA,uBAAA;EACA,uBAAA;EACA,gBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;EZiyCD;AY5yCD;EAeI,YAAA;EACA,oBAAA;EACA,gBAAA;EACA,uBAAA;EACA,+BAAA;EACA,kBAAA;EZgyCH;AY3xCD;EACE,mBAAA;EACA,oBAAA;EZ6xCD;Aat1CD;ECHE,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,qBAAA;Ed41CD;Aat1CC;EAAA;IAFE,cAAA;Ib41CD;EACF;Aax1CC;EAAA;IAFE,cAAA;Ib81CD;EACF;Aa11CD;EAAA;IAFI,eAAA;Ibg2CD;EACF;Aav1CD;ECvBE,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,qBAAA;Edi3CD;Aap1CD;ECvBE,oBAAA;EACA,qBAAA;Ed82CD;Ae92CG;EACE,oBAAA;EAEA,iBAAA;EAEA,oBAAA;EACA,qBAAA;Ef82CL;Ae91CG;EACE,aAAA;Efg2CL;Aez1CC;EACE,aAAA;Ef21CH;Ae51CC;EACE,qBAAA;Ef81CH;Ae/1CC;EACE,qBAAA;Efi2CH;Ael2CC;EACE,YAAA;Efo2CH;Aer2CC;EACE,qBAAA;Efu2CH;Aex2CC;EACE,qBAAA;Ef02CH;Ae32CC;EACE,YAAA;Ef62CH;Ae92CC;EACE,qBAAA;Efg3CH;Aej3CC;EACE,qBAAA;Efm3CH;Aep3CC;EACE,YAAA;Efs3CH;Aev3CC;EACE,qBAAA;Efy3CH;Ae13CC;EACE,oBAAA;Ef43CH;Ae92CC;EACE,aAAA;Efg3CH;Aej3CC;EACE,qBAAA;Efm3CH;Aep3CC;EACE,qBAAA;Efs3CH;Aev3CC;EACE,YAAA;Efy3CH;Ae13CC;EACE,qBAAA;Ef43CH;Ae73CC;EACE,qBAAA;Ef+3CH;Aeh4CC;EACE,YAAA;Efk4CH;Aen4CC;EACE,qBAAA;Efq4CH;Aet4CC;EACE,qBAAA;Efw4CH;Aez4CC;EACE,YAAA;Ef24CH;Ae54CC;EACE,qBAAA;Ef84CH;Ae/4CC;EACE,oBAAA;Efi5CH;Ae74CC;EACE,aAAA;Ef+4CH;Ae/5CC;EACE,YAAA;Efi6CH;Ael6CC;EACE,oBAAA;Efo6CH;Aer6CC;EACE,oBAAA;Efu6CH;Aex6CC;EACE,WAAA;Ef06CH;Ae36CC;EACE,oBAAA;Ef66CH;Ae96CC;EACE,oBAAA;Efg7CH;Aej7CC;EACE,WAAA;Efm7CH;Aep7CC;EACE,oBAAA;Efs7CH;Aev7CC;EACE,oBAAA;Efy7CH;Ae17CC;EACE,WAAA;Ef47CH;Ae77CC;EACE,oBAAA;Ef+7CH;Aeh8CC;EACE,mBAAA;Efk8CH;Ae97CC;EACE,YAAA;Efg8CH;Ael7CC;EACE,mBAAA;Efo7CH;Aer7CC;EACE,2BAAA;Efu7CH;Aex7CC;EACE,2BAAA;Ef07CH;Ae37CC;EACE,kBAAA;Ef67CH;Ae97CC;EACE,2BAAA;Efg8CH;Aej8CC;EACE,2BAAA;Efm8CH;Aep8CC;EACE,kBAAA;Efs8CH;Aev8CC;EACE,2BAAA;Efy8CH;Ae18CC;EACE,2BAAA;Ef48CH;Ae78CC;EACE,kBAAA;Ef+8CH;Aeh9CC;EACE,2BAAA;Efk9CH;Aen9CC;EACE,0BAAA;Efq9CH;Aet9CC;EACE,iBAAA;Efw9CH;Aa59CD;EE9BI;IACE,aAAA;If6/CH;Eet/CD;IACE,aAAA;Ifw/CD;Eez/CD;IACE,qBAAA;If2/CD;Ee5/CD;IACE,qBAAA;If8/CD;Ee//CD;IACE,YAAA;IfigDD;EelgDD;IACE,qBAAA;IfogDD;EergDD;IACE,qBAAA;IfugDD;EexgDD;IACE,YAAA;If0gDD;Ee3gDD;IACE,qBAAA;If6gDD;Ee9gDD;IACE,qBAAA;IfghDD;EejhDD;IACE,YAAA;IfmhDD;EephDD;IACE,qBAAA;IfshDD;EevhDD;IACE,oBAAA;IfyhDD;Ee3gDD;IACE,aAAA;If6gDD;Ee9gDD;IACE,qBAAA;IfghDD;EejhDD;IACE,qBAAA;IfmhDD;EephDD;IACE,YAAA;IfshDD;EevhDD;IACE,qBAAA;IfyhDD;Ee1hDD;IACE,qBAAA;If4hDD;Ee7hDD;IACE,YAAA;If+hDD;EehiDD;IACE,qBAAA;IfkiDD;EeniDD;IACE,qBAAA;IfqiDD;EetiDD;IACE,YAAA;IfwiDD;EeziDD;IACE,qBAAA;If2iDD;Ee5iDD;IACE,oBAAA;If8iDD;Ee1iDD;IACE,aAAA;If4iDD;Ee5jDD;IACE,YAAA;If8jDD;Ee/jDD;IACE,oBAAA;IfikDD;EelkDD;IACE,oBAAA;IfokDD;EerkDD;IACE,WAAA;IfukDD;EexkDD;IACE,oBAAA;If0kDD;Ee3kDD;IACE,oBAAA;If6kDD;Ee9kDD;IACE,WAAA;IfglDD;EejlDD;IACE,oBAAA;IfmlDD;EeplDD;IACE,oBAAA;IfslDD;EevlDD;IACE,WAAA;IfylDD;Ee1lDD;IACE,oBAAA;If4lDD;Ee7lDD;IACE,mBAAA;If+lDD;Ee3lDD;IACE,YAAA;If6lDD;Ee/kDD;IACE,mBAAA;IfilDD;EellDD;IACE,2BAAA;IfolDD;EerlDD;IACE,2BAAA;IfulDD;EexlDD;IACE,kBAAA;If0lDD;Ee3lDD;IACE,2BAAA;If6lDD;Ee9lDD;IACE,2BAAA;IfgmDD;EejmDD;IACE,kBAAA;IfmmDD;EepmDD;IACE,2BAAA;IfsmDD;EevmDD;IACE,2BAAA;IfymDD;Ee1mDD;IACE,kBAAA;If4mDD;Ee7mDD;IACE,2BAAA;If+mDD;EehnDD;IACE,0BAAA;IfknDD;EennDD;IACE,iBAAA;IfqnDD;EACF;AajnDD;EEvCI;IACE,aAAA;If2pDH;EeppDD;IACE,aAAA;IfspDD;EevpDD;IACE,qBAAA;IfypDD;Ee1pDD;IACE,qBAAA;If4pDD;Ee7pDD;IACE,YAAA;If+pDD;EehqDD;IACE,qBAAA;IfkqDD;EenqDD;IACE,qBAAA;IfqqDD;EetqDD;IACE,YAAA;IfwqDD;EezqDD;IACE,qBAAA;If2qDD;Ee5qDD;IACE,qBAAA;If8qDD;Ee/qDD;IACE,YAAA;IfirDD;EelrDD;IACE,qBAAA;IforDD;EerrDD;IACE,oBAAA;IfurDD;EezqDD;IACE,aAAA;If2qDD;Ee5qDD;IACE,qBAAA;If8qDD;Ee/qDD;IACE,qBAAA;IfirDD;EelrDD;IACE,YAAA;IforDD;EerrDD;IACE,qBAAA;IfurDD;EexrDD;IACE,qBAAA;If0rDD;Ee3rDD;IACE,YAAA;If6rDD;Ee9rDD;IACE,qBAAA;IfgsDD;EejsDD;IACE,qBAAA;IfmsDD;EepsDD;IACE,YAAA;IfssDD;EevsDD;IACE,qBAAA;IfysDD;Ee1sDD;IACE,oBAAA;If4sDD;EexsDD;IACE,aAAA;If0sDD;Ee1tDD;IACE,YAAA;If4tDD;Ee7tDD;IACE,oBAAA;If+tDD;EehuDD;IACE,oBAAA;IfkuDD;EenuDD;IACE,WAAA;IfquDD;EetuDD;IACE,oBAAA;IfwuDD;EezuDD;IACE,oBAAA;If2uDD;Ee5uDD;IACE,WAAA;If8uDD;Ee/uDD;IACE,oBAAA;IfivDD;EelvDD;IACE,oBAAA;IfovDD;EervDD;IACE,WAAA;IfuvDD;EexvDD;IACE,oBAAA;If0vDD;Ee3vDD;IACE,mBAAA;If6vDD;EezvDD;IACE,YAAA;If2vDD;Ee7uDD;IACE,mBAAA;If+uDD;EehvDD;IACE,2BAAA;IfkvDD;EenvDD;IACE,2BAAA;IfqvDD;EetvDD;IACE,kBAAA;IfwvDD;EezvDD;IACE,2BAAA;If2vDD;Ee5vDD;IACE,2BAAA;If8vDD;Ee/vDD;IACE,kBAAA;IfiwDD;EelwDD;IACE,2BAAA;IfowDD;EerwDD;IACE,2BAAA;IfuwDD;EexwDD;IACE,kBAAA;If0wDD;Ee3wDD;IACE,2BAAA;If6wDD;Ee9wDD;IACE,0BAAA;IfgxDD;EejxDD;IACE,iBAAA;IfmxDD;EACF;AaxwDD;EE9CI;IACE,aAAA;IfyzDH;EelzDD;IACE,aAAA;IfozDD;EerzDD;IACE,qBAAA;IfuzDD;EexzDD;IACE,qBAAA;If0zDD;Ee3zDD;IACE,YAAA;If6zDD;Ee9zDD;IACE,qBAAA;Ifg0DD;Eej0DD;IACE,qBAAA;Ifm0DD;Eep0DD;IACE,YAAA;Ifs0DD;Eev0DD;IACE,qBAAA;Ify0DD;Ee10DD;IACE,qBAAA;If40DD;Ee70DD;IACE,YAAA;If+0DD;Eeh1DD;IACE,qBAAA;Ifk1DD;Een1DD;IACE,oBAAA;Ifq1DD;Eev0DD;IACE,aAAA;Ify0DD;Ee10DD;IACE,qBAAA;If40DD;Ee70DD;IACE,qBAAA;If+0DD;Eeh1DD;IACE,YAAA;Ifk1DD;Een1DD;IACE,qBAAA;Ifq1DD;Eet1DD;IACE,qBAAA;Ifw1DD;Eez1DD;IACE,YAAA;If21DD;Ee51DD;IACE,qBAAA;If81DD;Ee/1DD;IACE,qBAAA;Ifi2DD;Eel2DD;IACE,YAAA;Ifo2DD;Eer2DD;IACE,qBAAA;Ifu2DD;Eex2DD;IACE,oBAAA;If02DD;Eet2DD;IACE,aAAA;Ifw2DD;Eex3DD;IACE,YAAA;If03DD;Ee33DD;IACE,oBAAA;If63DD;Ee93DD;IACE,oBAAA;Ifg4DD;Eej4DD;IACE,WAAA;Ifm4DD;Eep4DD;IACE,oBAAA;Ifs4DD;Eev4DD;IACE,oBAAA;Ify4DD;Ee14DD;IACE,WAAA;If44DD;Ee74DD;IACE,oBAAA;If+4DD;Eeh5DD;IACE,oBAAA;Ifk5DD;Een5DD;IACE,WAAA;Ifq5DD;Eet5DD;IACE,oBAAA;Ifw5DD;Eez5DD;IACE,mBAAA;If25DD;Eev5DD;IACE,YAAA;Ify5DD;Ee34DD;IACE,mBAAA;If64DD;Ee94DD;IACE,2BAAA;Ifg5DD;Eej5DD;IACE,2BAAA;Ifm5DD;Eep5DD;IACE,kBAAA;Ifs5DD;Eev5DD;IACE,2BAAA;Ify5DD;Ee15DD;IACE,2BAAA;If45DD;Ee75DD;IACE,kBAAA;If+5DD;Eeh6DD;IACE,2BAAA;Ifk6DD;Een6DD;IACE,2BAAA;Ifq6DD;Eet6DD;IACE,kBAAA;Ifw6DD;Eez6DD;IACE,2BAAA;If26DD;Ee56DD;IACE,0BAAA;If86DD;Ee/6DD;IACE,iBAAA;Ifi7DD;EACF;AgBr/DD;EACE,+BAAA;EhBu/DD;AgBr/DD;EACE,kBAAA;EhBu/DD;AgBj/DD;EACE,aAAA;EACA,iBAAA;EACA,qBAAA;EhBm/DD;AgBt/DD;;;;;;EAWQ,cAAA;EACA,yBAAA;EACA,qBAAA;EACA,+BAAA;EhBm/DP;AgBjgED;EAoBI,wBAAA;EACA,kCAAA;EhBg/DH;AgBrgED;;;;;;EA8BQ,eAAA;EhB++DP;AgB7gED;EAoCI,+BAAA;EhB4+DH;AgBhhED;EAyCI,2BAAA;EhB0+DH;AgBn+DD;;;;;;EAOQ,cAAA;EhBo+DP;AgBz9DD;EACE,2BAAA;EhB29DD;AgB59DD;;;;;;EAQQ,2BAAA;EhB49DP;AgBp+DD;;EAeM,0BAAA;EhBy9DL;AgB/8DD;;EAIM,2BAAA;EhB+8DL;AgBr8DD;;EAIM,2BAAA;EhBq8DL;AgB37DD;EACE,kBAAA;EACA,aAAA;EACA,uBAAA;EhB67DD;AgBx7DG;;EACE,kBAAA;EACA,aAAA;EACA,qBAAA;EhB27DL;AiBvkEC;;;;;;;;;;;;EAOI,2BAAA;EjB8kEL;AiBxkEC;;;;;EAMI,2BAAA;EjBykEL;AiB5lEC;;;;;;;;;;;;EAOI,2BAAA;EjBmmEL;AiB7lEC;;;;;EAMI,2BAAA;EjB8lEL;AiBjnEC;;;;;;;;;;;;EAOI,2BAAA;EjBwnEL;AiBlnEC;;;;;EAMI,2BAAA;EjBmnEL;AiBtoEC;;;;;;;;;;;;EAOI,2BAAA;EjB6oEL;AiBvoEC;;;;;EAMI,2BAAA;EjBwoEL;AiB3pEC;;;;;;;;;;;;EAOI,2BAAA;EjBkqEL;AiB5pEC;;;;;EAMI,2BAAA;EjB6pEL;AgB78DD;EAAA;IA5DI,aAAA;IACA,qBAAA;IACA,oBAAA;IACA,kBAAA;IACA,8CAAA;IACA,2BAAA;IACA,mCAAA;IhB6gED;EgBv9DH;IAlDM,kBAAA;IhB4gEH;EgB19DH;;;;;;IAzCY,qBAAA;IhB2gET;EgBl+DH;IAjCM,WAAA;IhBsgEH;EgBr+DH;;;;;;IAxBY,gBAAA;IhBqgET;EgB7+DH;;;;;;IApBY,iBAAA;IhBygET;EgBr/DH;;;;IAPY,kBAAA;IhBkgET;EACF;AkB3tED;EACE,YAAA;EACA,WAAA;EACA,WAAA;EAIA,cAAA;ElB0tED;AkBvtED;EACE,gBAAA;EACA,aAAA;EACA,YAAA;EACA,qBAAA;EACA,iBAAA;EACA,sBAAA;EACA,gBAAA;EACA,WAAA;EACA,kCAAA;ElBytED;AkBttED;EACE,uBAAA;EACA,iBAAA;EACA,oBAAA;EACA,mBAAA;ElBwtED;AkB7sED;Eb4BE,gCAAA;EACG,6BAAA;EACK,wBAAA;ELorET;AkB7sED;;EAEE,iBAAA;EACA,oBAAA;EACA,qBAAA;ElB+sED;AkB3sED;EACE,gBAAA;ElB6sED;AkBzsED;EACE,gBAAA;EACA,aAAA;ElB2sED;AkBvsED;;EAEE,cAAA;ElBysED;AkBrsED;;;EZxEE,sBAAA;EAEA,4CAAA;EACA,sBAAA;ENixED;AkBrsED;EACE,gBAAA;EACA,kBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;ElBusED;AkB7qED;EACE,gBAAA;EACA,aAAA;EACA,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,2BAAA;EACA,wBAAA;EACA,2BAAA;EACA,oBAAA;EbzDA,0DAAA;EACQ,kDAAA;EAsHR,wFAAA;EACK,2EAAA;EACG,wEAAA;ELonET;AmB7vEC;EACE,uBAAA;EACA,YAAA;EdcF,wFAAA;EACQ,gFAAA;ELkvET;AKltEC;EAAgC,gBAAA;EACA,YAAA;ELqtEjC;AKptEC;EAAgC,gBAAA;ELutEjC;AKttEC;EAAgC,gBAAA;ELytEjC;AkBrrEC;;;EAGE,qBAAA;EACA,2BAAA;EACA,YAAA;ElBurEH;AkBnrEC;EACE,cAAA;ElBqrEH;AkBzqED;EACE,0BAAA;ElB2qED;AkB/pED;;;;EAIE,mBAAA;EAEA,4BAAA;ElBgqED;AkB9pEC;;;;EACE,mBAAA;ElBmqEH;AkBjqEC;;;;EACE,mBAAA;ElBsqEH;AkB5pED;EACE,qBAAA;ElB8pED;AkBtpED;;EAEE,oBAAA;EACA,gBAAA;EACA,kBAAA;EACA,kBAAA;EACA,qBAAA;ElBwpED;AkB9pED;;EASI,oBAAA;EACA,kBAAA;EACA,qBAAA;EACA,iBAAA;ElBypEH;AkBtpED;;;;EAIE,oBAAA;EACA,oBAAA;EACA,oBAAA;ElBwpED;AkBrpED;;EAEE,kBAAA;ElBupED;AkBnpED;;EAEE,uBAAA;EACA,oBAAA;EACA,kBAAA;EACA,wBAAA;EACA,qBAAA;EACA,iBAAA;ElBqpED;AkBnpED;;EAEE,eAAA;EACA,mBAAA;ElBqpED;AkB5oEC;;;;;;EAGE,qBAAA;ElBipEH;AkB3oEC;;;;EAEE,qBAAA;ElB+oEH;AkBzoEC;;;;EAGI,qBAAA;ElB4oEL;AkBjoED;EAEE,kBAAA;EACA,qBAAA;EAEA,kBAAA;ElBioED;AkB/nEC;;EAEE,iBAAA;EACA,kBAAA;ElBioEH;AkBvnED;;ECnPE,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;EnB82ED;AmB52EC;EACE,cAAA;EACA,mBAAA;EnB82EH;AmB32EC;;EAEE,cAAA;EnB62EH;AkBnoED;;ECvPE,cAAA;EACA,oBAAA;EACA,iBAAA;EACA,mBAAA;EACA,oBAAA;EnB83ED;AmB53EC;EACE,cAAA;EACA,mBAAA;EnB83EH;AmB33EC;;EAEE,cAAA;EnB63EH;AkB1oED;EAEE,oBAAA;ElB2oED;AkB7oED;EAMI,uBAAA;ElB0oEH;AkBtoED;EACE,oBAAA;EACA,WAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;EACA,aAAA;EACA,cAAA;EACA,mBAAA;EACA,oBAAA;ElBwoED;AkBtoED;EACE,aAAA;EACA,cAAA;EACA,mBAAA;ElBwoED;AkBtoED;EACE,aAAA;EACA,cAAA;EACA,mBAAA;ElBwoED;AkBpoED;;;;;;ECrVI,gBAAA;EnBi+EH;AkB5oED;ECjVI,uBAAA;EdmDF,0DAAA;EACQ,kDAAA;EL86ET;AmBh+EG;EACE,uBAAA;EdgDJ,2EAAA;EACQ,mEAAA;ELm7ET;AkBtpED;ECvUI,gBAAA;EACA,uBAAA;EACA,2BAAA;EnBg+EH;AkB3pED;ECjUI,gBAAA;EnB+9EH;AkB3pED;;;;;;ECxVI,gBAAA;EnB2/EH;AkBnqED;ECpVI,uBAAA;EdmDF,0DAAA;EACQ,kDAAA;ELw8ET;AmB1/EG;EACE,uBAAA;EdgDJ,2EAAA;EACQ,mEAAA;EL68ET;AkB7qED;EC1UI,gBAAA;EACA,uBAAA;EACA,2BAAA;EnB0/EH;AkBlrED;ECpUI,gBAAA;EnBy/EH;AkBlrED;;;;;;EC3VI,gBAAA;EnBqhFH;AkB1rED;ECvVI,uBAAA;EdmDF,0DAAA;EACQ,kDAAA;ELk+ET;AmBphFG;EACE,uBAAA;EdgDJ,2EAAA;EACQ,mEAAA;ELu+ET;AkBpsED;EC7UI,gBAAA;EACA,uBAAA;EACA,2BAAA;EnBohFH;AkBzsED;ECvUI,gBAAA;EnBmhFH;AkBtsED;EACE,QAAA;ElBwsED;AkB/rED;EACE,gBAAA;EACA,iBAAA;EACA,qBAAA;EACA,gBAAA;ElBisED;AkB9mED;EAAA;IA7DM,uBAAA;IACA,kBAAA;IACA,wBAAA;IlB+qEH;EkBpnEH;IAtDM,uBAAA;IACA,aAAA;IACA,wBAAA;IlB6qEH;EkBznEH;IAhDM,uBAAA;IACA,wBAAA;IlB4qEH;EkB7nEH;;;IA1CQ,aAAA;IlB4qEL;EkBloEH;IApCM,aAAA;IlByqEH;EkBroEH;IAhCM,kBAAA;IACA,wBAAA;IlBwqEH;EkBzoEH;;IAvBM,uBAAA;IACA,eAAA;IACA,kBAAA;IACA,wBAAA;IlBoqEH;EkBhpEH;;IAjBQ,iBAAA;IlBqqEL;EkBppEH;;IAZM,oBAAA;IACA,gBAAA;IlBoqEH;EkBzpEH;IAHM,QAAA;IlB+pEH;EACF;AkBrpED;;;;EASI,eAAA;EACA,kBAAA;EACA,kBAAA;ElBkpEH;AkB7pED;;EAiBI,kBAAA;ElBgpEH;AkBjqED;EJxcE,oBAAA;EACA,qBAAA;Ed4mFD;AkBloEC;EAAA;IANI,mBAAA;IACA,kBAAA;IACA,kBAAA;IlB4oEH;EACF;AkB5qED;EAwCI,QAAA;EACA,aAAA;ElBuoEH;AkB1nEG;EAAA;IAHI,qBAAA;IlBioEL;EACF;AkBrnEG;EAAA;IAHI,kBAAA;IlB4nEL;EACF;AoBzoFD;EACE,uBAAA;EACA,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,wBAAA;EACA,iBAAA;EACA,wBAAA;EACA,+BAAA;EACA,qBAAA;EC4BA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,oBAAA;EhB2KA,2BAAA;EACG,wBAAA;EACC,uBAAA;EACI,mBAAA;ELs8ET;AoB5oFG;;;EdpBF,sBAAA;EAEA,4CAAA;EACA,sBAAA;ENoqFD;AoB9oFC;;EAEE,gBAAA;EACA,uBAAA;EpBgpFH;AoB7oFC;;EAEE,YAAA;EACA,wBAAA;Ef8BF,0DAAA;EACQ,kDAAA;ELknFT;AoB7oFC;;;EAGE,qBAAA;EACA,sBAAA;EE3CF,eAAA;EAGA,2BAAA;EjB8DA,0BAAA;EACQ,kBAAA;EL4nFT;AoBzoFD;EClDE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErB8rFD;AqB5rFC;;;;;EAKE,gBAAA;EACA,2BAAA;EACI,uBAAA;ErB8rFP;AqB5rFC;;;EAGE,wBAAA;ErB8rFH;AqBzrFG;;;;;;;;;;;;;;;EAKE,2BAAA;EACI,uBAAA;ErBqsFT;AoB9qFD;EClBI,gBAAA;EACA,2BAAA;ErBmsFH;AoB/qFD;ECrDE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBuuFD;AqBruFC;;;;;EAKE,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBuuFP;AqBruFC;;;EAGE,wBAAA;ErBuuFH;AqBluFG;;;;;;;;;;;;;;;EAKE,2BAAA;EACI,uBAAA;ErB8uFT;AoBptFD;ECrBI,gBAAA;EACA,2BAAA;ErB4uFH;AoBptFD;ECzDE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBgxFD;AqB9wFC;;;;;EAKE,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBgxFP;AqB9wFC;;;EAGE,wBAAA;ErBgxFH;AqB3wFG;;;;;;;;;;;;;;;EAKE,2BAAA;EACI,uBAAA;ErBuxFT;AoBzvFD;ECzBI,gBAAA;EACA,2BAAA;ErBqxFH;AoBzvFD;EC7DE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErByzFD;AqBvzFC;;;;;EAKE,gBAAA;EACA,2BAAA;EACI,uBAAA;ErByzFP;AqBvzFC;;;EAGE,wBAAA;ErByzFH;AqBpzFG;;;;;;;;;;;;;;;EAKE,2BAAA;EACI,uBAAA;ErBg0FT;AoB9xFD;EC7BI,gBAAA;EACA,2BAAA;ErB8zFH;AoB9xFD;ECjEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBk2FD;AqBh2FC;;;;;EAKE,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBk2FP;AqBh2FC;;;EAGE,wBAAA;ErBk2FH;AqB71FG;;;;;;;;;;;;;;;EAKE,2BAAA;EACI,uBAAA;ErBy2FT;AoBn0FD;ECjCI,gBAAA;EACA,2BAAA;ErBu2FH;AoBn0FD;ECrEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErB24FD;AqBz4FC;;;;;EAKE,gBAAA;EACA,2BAAA;EACI,uBAAA;ErB24FP;AqBz4FC;;;EAGE,wBAAA;ErB24FH;AqBt4FG;;;;;;;;;;;;;;;EAKE,2BAAA;EACI,uBAAA;ErBk5FT;AoBx2FD;ECrCI,gBAAA;EACA,2BAAA;ErBg5FH;AoBn2FD;EACE,gBAAA;EACA,qBAAA;EACA,iBAAA;EACA,kBAAA;EpBq2FD;AoBn2FC;;;;EAIE,+BAAA;Ef1BF,0BAAA;EACQ,kBAAA;ELg4FT;AoBp2FC;;;;EAIE,2BAAA;EpBs2FH;AoBp2FC;;EAEE,gBAAA;EACA,4BAAA;EACA,+BAAA;EpBs2FH;AoBl2FG;;;;EAEE,gBAAA;EACA,uBAAA;EpBs2FL;AoB71FD;;EC9EE,oBAAA;EACA,iBAAA;EACA,mBAAA;EACA,oBAAA;ErB+6FD;AoBh2FD;;EClFE,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;ErBs7FD;AoBn2FD;;ECtFE,kBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;ErB67FD;AoBl2FD;EACE,gBAAA;EACA,aAAA;EpBo2FD;AoBh2FD;EACE,iBAAA;EpBk2FD;AoB31FC;;;EACE,aAAA;EpB+1FH;AuBh/FD;EACE,YAAA;ElBiLA,0CAAA;EACK,qCAAA;EACG,kCAAA;ELk0FT;AuBn/FC;EACE,YAAA;EvBq/FH;AuBj/FD;EACE,eAAA;EvBm/FD;AuBj/FC;EAAY,gBAAA;EvBo/Fb;AuBn/FC;EAAY,oBAAA;EvBs/Fb;AuBr/FC;EAAY,0BAAA;EvBw/Fb;AuBr/FD;EACE,oBAAA;EACA,WAAA;EACA,kBAAA;ElB+JA,uCAAA;EACK,kCAAA;EACG,+BAAA;ELy1FT;AwBhhGD;EACE,uBAAA;EACA,UAAA;EACA,WAAA;EACA,kBAAA;EACA,wBAAA;EACA,uBAAA;EACA,qCAAA;EACA,oCAAA;ExBkhGD;AwB9gGD;EACE,oBAAA;ExBghGD;AwB5gGD;EACE,YAAA;ExB8gGD;AwB1gGD;EACE,oBAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EACA,eAAA;EACA,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,kBAAA;EACA,iBAAA;EACA,kBAAA;EACA,2BAAA;EACA,2BAAA;EACA,uCAAA;EACA,oBAAA;EnBwBA,qDAAA;EACQ,6CAAA;EmBvBR,sCAAA;EAAA,8BAAA;ExB6gGD;AwBxgGC;EACE,UAAA;EACA,YAAA;ExB0gGH;AwBniGD;ECvBE,aAAA;EACA,eAAA;EACA,kBAAA;EACA,2BAAA;EzB6jGD;AwBziGD;EAmCI,gBAAA;EACA,mBAAA;EACA,aAAA;EACA,qBAAA;EACA,yBAAA;EACA,gBAAA;EACA,qBAAA;ExBygGH;AwBngGC;;EAEE,uBAAA;EACA,gBAAA;EACA,2BAAA;ExBqgGH;AwB//FC;;;EAGE,gBAAA;EACA,uBAAA;EACA,YAAA;EACA,2BAAA;ExBigGH;AwBx/FC;;;EAGE,gBAAA;ExB0/FH;AwBr/FC;;EAEE,uBAAA;EACA,+BAAA;EACA,wBAAA;EE1GF,qEAAA;EF4GE,qBAAA;ExBu/FH;AwBl/FD;EAGI,gBAAA;ExBk/FH;AwBr/FD;EAQI,YAAA;ExBg/FH;AwBx+FD;EACE,YAAA;EACA,UAAA;ExB0+FD;AwBl+FD;EACE,SAAA;EACA,aAAA;ExBo+FD;AwBh+FD;EACE,gBAAA;EACA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,qBAAA;ExBk+FD;AwB99FD;EACE,iBAAA;EACA,SAAA;EACA,UAAA;EACA,WAAA;EACA,QAAA;EACA,cAAA;ExBg+FD;AwB59FD;EACE,UAAA;EACA,YAAA;ExB89FD;AwBt9FD;;EAII,eAAA;EACA,0BAAA;EACA,aAAA;ExBs9FH;AwB59FD;;EAUI,WAAA;EACA,cAAA;EACA,oBAAA;ExBs9FH;AwBh8FD;EAZE;IAnEA,YAAA;IACA,UAAA;IxBmhGC;EwBj9FD;IAzDA,SAAA;IACA,aAAA;IxB6gGC;EACF;A2B5pGD;;EAEE,oBAAA;EACA,uBAAA;EACA,wBAAA;E3B8pGD;A2BlqGD;;EAMI,oBAAA;EACA,aAAA;E3BgqGH;A2B9pGG;;;;;;;;EAIE,YAAA;E3BoqGL;A2BlqGG;;EAEE,YAAA;E3BoqGL;A2B9pGD;;;;EAKI,mBAAA;E3B+pGH;A2B1pGD;EACE,mBAAA;E3B4pGD;A2B7pGD;;EAMI,aAAA;E3B2pGH;A2BjqGD;;;EAWI,kBAAA;E3B2pGH;A2BvpGD;EACE,kBAAA;E3BypGD;A2BrpGD;EACE,gBAAA;E3BupGD;A2BtpGC;ECrDA,+BAAA;EACG,4BAAA;E5B8sGJ;A2BrpGD;;EClDE,8BAAA;EACG,2BAAA;E5B2sGJ;A2BppGD;EACE,aAAA;E3BspGD;A2BppGD;EACE,kBAAA;E3BspGD;A2BppGD;;ECtEE,+BAAA;EACG,4BAAA;E5B8tGJ;A2BnpGD;ECpEE,8BAAA;EACG,2BAAA;E5B0tGJ;A2BlpGD;;EAEE,YAAA;E3BopGD;A2BnoGD;EACE,mBAAA;EACA,oBAAA;E3BqoGD;A2BnoGD;EACE,oBAAA;EACA,qBAAA;E3BqoGD;A2BhoGD;EtBlDE,0DAAA;EACQ,kDAAA;ELqrGT;A2BhoGC;EtBtDA,0BAAA;EACQ,kBAAA;ELyrGT;A2B7nGD;EACE,gBAAA;E3B+nGD;A2B5nGD;EACE,yBAAA;EACA,wBAAA;E3B8nGD;A2B3nGD;EACE,yBAAA;E3B6nGD;A2BtnGD;;;EAII,gBAAA;EACA,aAAA;EACA,aAAA;EACA,iBAAA;E3BunGH;A2B9nGD;EAcM,aAAA;E3BmnGL;A2BjoGD;;;;EAsBI,kBAAA;EACA,gBAAA;E3BinGH;A2B5mGC;EACE,kBAAA;E3B8mGH;A2B5mGC;EACE,8BAAA;ECvKF,+BAAA;EACC,8BAAA;E5BsxGF;A2B7mGC;EACE,gCAAA;ECnLF,4BAAA;EACC,2BAAA;E5BmyGF;A2B7mGD;EACE,kBAAA;E3B+mGD;A2B7mGD;;EClLE,+BAAA;EACC,8BAAA;E5BmyGF;A2B5mGD;EChME,4BAAA;EACC,2BAAA;E5B+yGF;A2BvmGD;EACE,gBAAA;EACA,aAAA;EACA,qBAAA;EACA,2BAAA;E3BymGD;A2B7mGD;;EAOI,aAAA;EACA,qBAAA;EACA,WAAA;E3B0mGH;A2BnnGD;EAYI,aAAA;E3B0mGH;A2BtnGD;EAgBI,YAAA;E3BymGH;A2B3lGD;;EAEE,oBAAA;EACA,aAAA;EL1OA,YAAA;EAGA,0BAAA;EtBs0GD;A6Bt0GD;EACE,oBAAA;EACA,gBAAA;EACA,2BAAA;E7Bw0GD;A6Br0GC;EACE,aAAA;EACA,iBAAA;EACA,kBAAA;E7Bu0GH;A6Bh1GD;EAeI,oBAAA;EACA,YAAA;EAKA,aAAA;EAEA,aAAA;EACA,kBAAA;E7B+zGH;A6BtzGD;;;EV0BE,cAAA;EACA,oBAAA;EACA,iBAAA;EACA,mBAAA;EACA,oBAAA;EnBiyGD;AmB/xGC;;;EACE,cAAA;EACA,mBAAA;EnBmyGH;AmBhyGC;;;;;;EAEE,cAAA;EnBsyGH;A6Bx0GD;;;EVqBE,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;EnBwzGD;AmBtzGC;;;EACE,cAAA;EACA,mBAAA;EnB0zGH;AmBvzGC;;;;;;EAEE,cAAA;EnB6zGH;A6Bt1GD;;;EAGE,qBAAA;E7Bw1GD;A6Bt1GC;;;EACE,kBAAA;E7B01GH;A6Bt1GD;;EAEE,WAAA;EACA,qBAAA;EACA,wBAAA;E7Bw1GD;A6Bn1GD;EACE,mBAAA;EACA,iBAAA;EACA,qBAAA;EACA,gBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;E7Bq1GD;A6Bl1GC;EACE,mBAAA;EACA,iBAAA;EACA,oBAAA;E7Bo1GH;A6Bl1GC;EACE,oBAAA;EACA,iBAAA;EACA,oBAAA;E7Bo1GH;A6Bx2GD;;EA0BI,eAAA;E7Bk1GH;A6B70GD;;;;;;;EDhGE,+BAAA;EACG,4BAAA;E5Bs7GJ;A6B90GD;EACE,iBAAA;E7Bg1GD;A6B90GD;;;;;;;EDpGE,8BAAA;EACG,2BAAA;E5B27GJ;A6B/0GD;EACE,gBAAA;E7Bi1GD;A6B50GD;EACE,oBAAA;EAGA,cAAA;EACA,qBAAA;E7B40GD;A6Bj1GD;EAUI,oBAAA;E7B00GH;A6Bp1GD;EAYM,mBAAA;E7B20GL;A6Bx0GG;;;EAGE,YAAA;E7B00GL;A6Br0GC;;EAGI,oBAAA;E7Bs0GL;A6Bn0GC;;EAGI,mBAAA;E7Bo0GL;A8B99GD;EACE,kBAAA;EACA,iBAAA;EACA,kBAAA;E9Bg+GD;A8Bn+GD;EAOI,oBAAA;EACA,gBAAA;E9B+9GH;A8Bv+GD;EAWM,oBAAA;EACA,gBAAA;EACA,oBAAA;E9B+9GL;A8B99GK;;EAEE,uBAAA;EACA,2BAAA;E9Bg+GP;A8B39GG;EACE,gBAAA;E9B69GL;A8B39GK;;EAEE,gBAAA;EACA,uBAAA;EACA,+BAAA;EACA,qBAAA;E9B69GP;A8Bt9GG;;;EAGE,2BAAA;EACA,uBAAA;E9Bw9GL;A8BjgHD;ELHE,aAAA;EACA,eAAA;EACA,kBAAA;EACA,2BAAA;EzBugHD;A8BvgHD;EA0DI,iBAAA;E9Bg9GH;A8Bv8GD;EACE,kCAAA;E9By8GD;A8B18GD;EAGI,aAAA;EAEA,qBAAA;E9By8GH;A8B98GD;EASM,mBAAA;EACA,yBAAA;EACA,+BAAA;EACA,4BAAA;E9Bw8GL;A8Bv8GK;EACE,uCAAA;E9By8GP;A8Bn8GK;;;EAGE,gBAAA;EACA,2BAAA;EACA,2BAAA;EACA,kCAAA;EACA,iBAAA;E9Bq8GP;A8Bh8GC;EAqDA,aAAA;EA8BA,kBAAA;E9Bi3GD;A8Bp8GC;EAwDE,aAAA;E9B+4GH;A8Bv8GC;EA0DI,oBAAA;EACA,oBAAA;E9Bg5GL;A8B38GC;EAgEE,WAAA;EACA,YAAA;E9B84GH;A8Bl4GD;EAAA;IAPM,qBAAA;IACA,WAAA;I9B64GH;E8Bv4GH;IAJQ,kBAAA;I9B84GL;EACF;A8Bx9GC;EAuFE,iBAAA;EACA,oBAAA;E9Bo4GH;A8B59GC;;;EA8FE,2BAAA;E9Bm4GH;A8Br3GD;EAAA;IATM,kCAAA;IACA,4BAAA;I9Bk4GH;E8B13GH;;;IAHM,8BAAA;I9Bk4GH;EACF;A8Bn+GD;EAEI,aAAA;E9Bo+GH;A8Bt+GD;EAMM,oBAAA;E9Bm+GL;A8Bz+GD;EASM,kBAAA;E9Bm+GL;A8B99GK;;;EAGE,gBAAA;EACA,2BAAA;E9Bg+GP;A8Bx9GD;EAEI,aAAA;E9By9GH;A8B39GD;EAIM,iBAAA;EACA,gBAAA;E9B09GL;A8B98GD;EACE,aAAA;E9Bg9GD;A8Bj9GD;EAII,aAAA;E9Bg9GH;A8Bp9GD;EAMM,oBAAA;EACA,oBAAA;E9Bi9GL;A8Bx9GD;EAYI,WAAA;EACA,YAAA;E9B+8GH;A8Bn8GD;EAAA;IAPM,qBAAA;IACA,WAAA;I9B88GH;E8Bx8GH;IAJQ,kBAAA;I9B+8GL;EACF;A8Bv8GD;EACE,kBAAA;E9By8GD;A8B18GD;EAKI,iBAAA;EACA,oBAAA;E9Bw8GH;A8B98GD;;;EAYI,2BAAA;E9Bu8GH;A8Bz7GD;EAAA;IATM,kCAAA;IACA,4BAAA;I9Bs8GH;E8B97GH;;;IAHM,8BAAA;I9Bs8GH;EACF;A8B77GD;EAEI,eAAA;E9B87GH;A8Bh8GD;EAKI,gBAAA;E9B87GH;A8Br7GD;EAEE,kBAAA;EF3OA,4BAAA;EACC,2BAAA;E5BkqHF;A+B5pHD;EACE,oBAAA;EACA,kBAAA;EACA,qBAAA;EACA,+BAAA;E/B8pHD;A+BtpHD;EAAA;IAFI,oBAAA;I/B4pHD;EACF;A+B7oHD;EAAA;IAFI,aAAA;I/BmpHD;EACF;A+BroHD;EACE,qBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mCAAA;EACA,4DAAA;EAAA,oDAAA;EAEA,mCAAA;E/BsoHD;A+BpoHC;EACE,kBAAA;E/BsoHH;A+B1mHD;EAAA;IAxBI,aAAA;IACA,eAAA;IACA,0BAAA;IAAA,kBAAA;I/BsoHD;E+BpoHC;IACE,2BAAA;IACA,yBAAA;IACA,mBAAA;IACA,8BAAA;I/BsoHH;E+BnoHC;IACE,qBAAA;I/BqoHH;E+BhoHC;;;IAGE,iBAAA;IACA,kBAAA;I/BkoHH;EACF;A+B9nHD;;EAGI,mBAAA;E/B+nHH;A+B1nHC;EAAA;;IAFI,mBAAA;I/BioHH;EACF;A+BxnHD;;;;EAII,qBAAA;EACA,oBAAA;E/B0nHH;A+BpnHC;EAAA;;;;IAHI,iBAAA;IACA,gBAAA;I/B8nHH;EACF;A+BlnHD;EACE,eAAA;EACA,uBAAA;E/BonHD;A+B/mHD;EAAA;IAFI,kBAAA;I/BqnHD;EACF;A+BjnHD;;EAEE,iBAAA;EACA,UAAA;EACA,SAAA;EACA,eAAA;E1BGA,yCAAA;EACQ,oCAAA;EAAA,iCAAA;ELinHT;A+B9mHD;EAAA;;IAFI,kBAAA;I/BqnHD;EACF;A+BnnHD;EACE,QAAA;EACA,uBAAA;E/BqnHD;A+BnnHD;EACE,WAAA;EACA,kBAAA;EACA,uBAAA;E/BqnHD;A+B/mHD;EACE,aAAA;EACA,oBAAA;EACA,iBAAA;EACA,mBAAA;EACA,cAAA;E/BinHD;A+B/mHC;;EAEE,uBAAA;E/BinHH;A+BxmHD;EALI;;IAEE,oBAAA;I/BgnHH;EACF;A+BtmHD;EACE,oBAAA;EACA,cAAA;EACA,oBAAA;EACA,mBAAA;EC3LA,iBAAA;EACA,oBAAA;ED4LA,+BAAA;EACA,wBAAA;EACA,+BAAA;EACA,oBAAA;E/BymHD;A+BrmHC;EACE,YAAA;E/BumHH;A+BrnHD;EAmBI,gBAAA;EACA,aAAA;EACA,aAAA;EACA,oBAAA;E/BqmHH;A+B3nHD;EAyBI,iBAAA;E/BqmHH;A+B/lHD;EAAA;IAFI,eAAA;I/BqmHD;EACF;A+B5lHD;EACE,qBAAA;E/B8lHD;A+B/lHD;EAII,mBAAA;EACA,sBAAA;EACA,mBAAA;E/B8lHH;A+BnkHC;EAAA;IArBI,kBAAA;IACA,aAAA;IACA,aAAA;IACA,eAAA;IACA,+BAAA;IACA,WAAA;IACA,0BAAA;IAAA,kBAAA;I/B4lHH;E+B7kHD;;IAZM,4BAAA;I/B6lHL;E+BjlHD;IATM,mBAAA;I/B6lHL;E+B5lHK;;IAEE,wBAAA;I/B8lHP;EACF;A+BxkHD;EAAA;IAfI,aAAA;IACA,WAAA;I/B2lHD;E+B7kHH;IAXM,aAAA;I/B2lHH;E+BhlHH;IATQ,mBAAA;IACA,sBAAA;I/B4lHL;E+BxlHC;IACE,qBAAA;I/B0lHH;EACF;A+BzkHD;EALE;IE9QA,wBAAA;IjCg2HC;E+BjlHD;IElRA,yBAAA;IjCs2HC;EACF;A+B5kHD;EACE,oBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mCAAA;EACA,sCAAA;E1B3OA,8FAAA;EACQ,sFAAA;E2B/DR,iBAAA;EACA,oBAAA;EhC03HD;AkBl7GD;EAAA;IA7DM,uBAAA;IACA,kBAAA;IACA,wBAAA;IlBm/GH;EkBx7GH;IAtDM,uBAAA;IACA,aAAA;IACA,wBAAA;IlBi/GH;EkB77GH;IAhDM,uBAAA;IACA,wBAAA;IlBg/GH;EkBj8GH;;;IA1CQ,aAAA;IlBg/GL;EkBt8GH;IApCM,aAAA;IlB6+GH;EkBz8GH;IAhCM,kBAAA;IACA,wBAAA;IlB4+GH;EkB78GH;;IAvBM,uBAAA;IACA,eAAA;IACA,kBAAA;IACA,wBAAA;IlBw+GH;EkBp9GH;;IAjBQ,iBAAA;IlBy+GL;EkBx9GH;;IAZM,oBAAA;IACA,gBAAA;IlBw+GH;EkB79GH;IAHM,QAAA;IlBm+GH;EACF;A+BtnHC;EAAA;IAFI,oBAAA;I/B4nHH;EACF;A+BvmHD;EAAA;IAbI,aAAA;IACA,WAAA;IACA,gBAAA;IACA,iBAAA;IACA,gBAAA;IACA,mBAAA;I1BlQF,0BAAA;IACQ,kBAAA;IL23HP;E+BtnHC;IACE,qBAAA;I/BwnHH;EACF;A+BhnHD;EACE,eAAA;EHlVA,4BAAA;EACC,2BAAA;E5Bq8HF;A+BhnHD;EH9UE,+BAAA;EACC,8BAAA;E5Bi8HF;A+B3mHD;EC5VE,iBAAA;EACA,oBAAA;EhC08HD;A+B5mHC;EC/VA,kBAAA;EACA,qBAAA;EhC88HD;A+B7mHC;EClWA,kBAAA;EACA,qBAAA;EhCk9HD;A+BvmHD;EC5WE,kBAAA;EACA,qBAAA;EhCs9HD;A+B9lHD;EAAA;IATI,aAAA;IACA,mBAAA;IACA,oBAAA;I/B2mHD;E+BxmHC;IACE,iBAAA;I/B0mHH;EACF;A+BlmHD;EACE,2BAAA;EACA,uBAAA;E/BomHD;A+BtmHD;EAKI,gBAAA;E/BomHH;A+BnmHG;;EAEE,gBAAA;EACA,+BAAA;E/BqmHL;A+B9mHD;EAcI,gBAAA;E/BmmHH;A+BjnHD;EAmBM,gBAAA;E/BimHL;A+B/lHK;;EAEE,gBAAA;EACA,+BAAA;E/BimHP;A+B7lHK;;;EAGE,gBAAA;EACA,2BAAA;E/B+lHP;A+B3lHK;;;EAGE,gBAAA;EACA,+BAAA;E/B6lHP;A+BroHD;EA8CI,uBAAA;E/B0lHH;A+BzlHG;;EAEE,2BAAA;E/B2lHL;A+B5oHD;EAoDM,2BAAA;E/B2lHL;A+B/oHD;;EA0DI,uBAAA;E/BylHH;A+BllHK;;;EAGE,2BAAA;EACA,gBAAA;E/BolHP;A+BnjHC;EAAA;IAzBQ,gBAAA;I/BglHP;E+B/kHO;;IAEE,gBAAA;IACA,+BAAA;I/BilHT;E+B7kHO;;;IAGE,gBAAA;IACA,2BAAA;I/B+kHT;E+B3kHO;;;IAGE,gBAAA;IACA,+BAAA;I/B6kHT;EACF;A+B/qHD;EA8GI,gBAAA;E/BokHH;A+BnkHG;EACE,gBAAA;E/BqkHL;A+BrrHD;EAqHI,gBAAA;E/BmkHH;A+BlkHG;;EAEE,gBAAA;E/BokHL;A+BhkHK;;;;EAEE,gBAAA;E/BokHP;A+B5jHD;EACE,2BAAA;EACA,uBAAA;E/B8jHD;A+BhkHD;EAKI,gBAAA;E/B8jHH;A+B7jHG;;EAEE,gBAAA;EACA,+BAAA;E/B+jHL;A+BxkHD;EAcI,gBAAA;E/B6jHH;A+B3kHD;EAmBM,gBAAA;E/B2jHL;A+BzjHK;;EAEE,gBAAA;EACA,+BAAA;E/B2jHP;A+BvjHK;;;EAGE,gBAAA;EACA,2BAAA;E/ByjHP;A+BrjHK;;;EAGE,gBAAA;EACA,+BAAA;E/BujHP;A+B/lHD;EA+CI,uBAAA;E/BmjHH;A+BljHG;;EAEE,2BAAA;E/BojHL;A+BtmHD;EAqDM,2BAAA;E/BojHL;A+BzmHD;;EA2DI,uBAAA;E/BkjHH;A+B5iHK;;;EAGE,2BAAA;EACA,gBAAA;E/B8iHP;A+BvgHC;EAAA;IA/BQ,uBAAA;I/B0iHP;E+B3gHD;IA5BQ,2BAAA;I/B0iHP;E+B9gHD;IAzBQ,gBAAA;I/B0iHP;E+BziHO;;IAEE,gBAAA;IACA,+BAAA;I/B2iHT;E+BviHO;;;IAGE,gBAAA;IACA,2BAAA;I/ByiHT;E+BriHO;;;IAGE,gBAAA;IACA,+BAAA;I/BuiHT;EACF;A+B/oHD;EA+GI,gBAAA;E/BmiHH;A+BliHG;EACE,gBAAA;E/BoiHL;A+BrpHD;EAsHI,gBAAA;E/BkiHH;A+BjiHG;;EAEE,gBAAA;E/BmiHL;A+B/hHK;;;;EAEE,gBAAA;E/BmiHP;AkCxqID;EACE,mBAAA;EACA,qBAAA;EACA,kBAAA;EACA,2BAAA;EACA,oBAAA;ElC0qID;AkC/qID;EAQI,uBAAA;ElC0qIH;AkClrID;EAWM,mBAAA;EACA,gBAAA;EACA,gBAAA;ElC0qIL;AkCvrID;EAkBI,gBAAA;ElCwqIH;AmC5rID;EACE,uBAAA;EACA,iBAAA;EACA,gBAAA;EACA,oBAAA;EnC8rID;AmClsID;EAOI,iBAAA;EnC8rIH;AmCrsID;;EAUM,oBAAA;EACA,aAAA;EACA,mBAAA;EACA,yBAAA;EACA,uBAAA;EACA,gBAAA;EACA,2BAAA;EACA,2BAAA;EACA,mBAAA;EnC+rIL;AmC7rIG;;EAGI,gBAAA;EPXN,gCAAA;EACG,6BAAA;E5B0sIJ;AmC5rIG;;EPvBF,iCAAA;EACG,8BAAA;E5ButIJ;AmCvrIG;;;;EAEE,gBAAA;EACA,2BAAA;EACA,uBAAA;EnC2rIL;AmCrrIG;;;;;;EAGE,YAAA;EACA,gBAAA;EACA,2BAAA;EACA,uBAAA;EACA,iBAAA;EnC0rIL;AmChvID;;;;;;EAiEM,gBAAA;EACA,2BAAA;EACA,uBAAA;EACA,qBAAA;EnCurIL;AmC9qID;;EC1EM,oBAAA;EACA,iBAAA;EpC4vIL;AoC1vIG;;ERMF,gCAAA;EACG,6BAAA;E5BwvIJ;AoCzvIG;;ERRF,iCAAA;EACG,8BAAA;E5BqwIJ;AmCxrID;;EC/EM,mBAAA;EACA,iBAAA;EpC2wIL;AoCzwIG;;ERMF,gCAAA;EACG,6BAAA;E5BuwIJ;AoCxwIG;;ERRF,iCAAA;EACG,8BAAA;E5BoxIJ;AqCvxID;EACE,iBAAA;EACA,gBAAA;EACA,kBAAA;EACA,oBAAA;ErCyxID;AqC7xID;EAOI,iBAAA;ErCyxIH;AqChyID;;EAUM,uBAAA;EACA,mBAAA;EACA,2BAAA;EACA,2BAAA;EACA,qBAAA;ErC0xIL;AqCxyID;;EAmBM,uBAAA;EACA,2BAAA;ErCyxIL;AqC7yID;;EA2BM,cAAA;ErCsxIL;AqCjzID;;EAkCM,aAAA;ErCmxIL;AqCrzID;;;;EA2CM,gBAAA;EACA,2BAAA;EACA,qBAAA;ErCgxIL;AsC9zID;EACE,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBAAA;EACA,oBAAA;EACA,qBAAA;EACA,0BAAA;EACA,sBAAA;EtCg0ID;AsC5zIG;;EAEE,gBAAA;EACA,uBAAA;EACA,iBAAA;EtC8zIL;AsCzzIC;EACE,eAAA;EtC2zIH;AsCvzIC;EACE,oBAAA;EACA,WAAA;EtCyzIH;AsClzID;ECtCE,2BAAA;EvC21ID;AuCx1IG;;EAEE,2BAAA;EvC01IL;AsCrzID;EC1CE,2BAAA;EvCk2ID;AuC/1IG;;EAEE,2BAAA;EvCi2IL;AsCxzID;EC9CE,2BAAA;EvCy2ID;AuCt2IG;;EAEE,2BAAA;EvCw2IL;AsC3zID;EClDE,2BAAA;EvCg3ID;AuC72IG;;EAEE,2BAAA;EvC+2IL;AsC9zID;ECtDE,2BAAA;EvCu3ID;AuCp3IG;;EAEE,2BAAA;EvCs3IL;AsCj0ID;EC1DE,2BAAA;EvC83ID;AuC33IG;;EAEE,2BAAA;EvC63IL;AwC/3ID;EACE,uBAAA;EACA,iBAAA;EACA,kBAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBAAA;EACA,0BAAA;EACA,qBAAA;EACA,oBAAA;EACA,2BAAA;EACA,qBAAA;ExCi4ID;AwC93IC;EACE,eAAA;ExCg4IH;AwC53IC;EACE,oBAAA;EACA,WAAA;ExC83IH;AwC53IC;EACE,QAAA;EACA,kBAAA;ExC83IH;AwCz3IG;;EAEE,gBAAA;EACA,uBAAA;EACA,iBAAA;ExC23IL;AwCt3IC;;EAEE,gBAAA;EACA,2BAAA;ExCw3IH;AwCt3IC;EACE,kBAAA;ExCw3IH;AyCv6ID;EACE,eAAA;EACA,qBAAA;EACA,gBAAA;EACA,2BAAA;EzCy6ID;AyC76ID;;EAQI,gBAAA;EzCy6IH;AyCj7ID;EAWI,qBAAA;EACA,iBAAA;EACA,kBAAA;EzCy6IH;AyCt7ID;EAiBI,2BAAA;EzCw6IH;AyCr6IC;EACE,oBAAA;EzCu6IH;AyC57ID;EAyBI,iBAAA;EzCs6IH;AyCr5ID;EAAA;IAbI,mBAAA;IACA,sBAAA;IzCs6ID;EyCp6IC;IACE,oBAAA;IACA,qBAAA;IzCs6IH;EyC95IH;;IAHM,iBAAA;IzCq6IH;EACF;A0C58ID;EACE,gBAAA;EACA,cAAA;EACA,qBAAA;EACA,yBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;ErC8KA,0CAAA;EACK,qCAAA;EACG,kCAAA;ELiyIT;A0Cx9ID;;EAaI,mBAAA;EACA,oBAAA;E1C+8IH;A0C38IC;;;EAGE,uBAAA;E1C68IH;A0Cl+ID;EA0BI,cAAA;EACA,gBAAA;E1C28IH;A2Cp+ID;EACE,eAAA;EACA,qBAAA;EACA,+BAAA;EACA,oBAAA;E3Cs+ID;A2C1+ID;EAQI,eAAA;EAEA,gBAAA;E3Co+IH;A2C9+ID;EAcI,mBAAA;E3Cm+IH;A2Cj/ID;;EAoBI,kBAAA;E3Ci+IH;A2Cr/ID;EAuBI,iBAAA;E3Ci+IH;A2Cz9ID;;EAEE,qBAAA;E3C29ID;A2C79ID;;EAMI,oBAAA;EACA,WAAA;EACA,cAAA;EACA,gBAAA;E3C29IH;A2Cn9ID;ECrDE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5C2gJD;A2Cx9ID;EChDI,2BAAA;E5C2gJH;A2C39ID;EC7CI,gBAAA;E5C2gJH;A2C39ID;ECxDE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5CshJD;A2Ch+ID;ECnDI,2BAAA;E5CshJH;A2Cn+ID;EChDI,gBAAA;E5CshJH;A2Cn+ID;EC3DE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5CiiJD;A2Cx+ID;ECtDI,2BAAA;E5CiiJH;A2C3+ID;ECnDI,gBAAA;E5CiiJH;A2C3+ID;EC9DE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5C4iJD;A2Ch/ID;ECzDI,2BAAA;E5C4iJH;A2Cn/ID;ECtDI,gBAAA;E5C4iJH;A6C9iJD;EACE;IAAQ,6BAAA;I7CijJP;E6ChjJD;IAAQ,0BAAA;I7CmjJP;EACF;A6ChjJD;EACE;IAAQ,6BAAA;I7CmjJP;E6CljJD;IAAQ,0BAAA;I7CqjJP;EACF;A6CxjJD;EACE;IAAQ,6BAAA;I7CmjJP;E6CljJD;IAAQ,0BAAA;I7CqjJP;EACF;A6C7iJD;EACE,kBAAA;EACA,cAAA;EACA,qBAAA;EACA,2BAAA;EACA,oBAAA;ExCqCA,wDAAA;EACQ,gDAAA;EL2gJT;A6C5iJD;EACE,aAAA;EACA,WAAA;EACA,cAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2BAAA;ExCwBA,wDAAA;EACQ,gDAAA;EAsHR,qCAAA;EACK,gCAAA;EACG,6BAAA;ELk6IT;A6CziJD;;ECAI,+MAAA;EACA,0MAAA;EACA,uMAAA;EDCF,oCAAA;EAAA,4BAAA;E7C6iJD;A6CtiJD;;ExC7CE,4DAAA;EACK,uDAAA;EACG,oDAAA;ELulJT;A6CriJC;;EAEE,iBAAA;E7CuiJH;A6CpiJC;EACE,gBAAA;EACA,iBAAA;EACA,+BAAA;EACA,wBAAA;EACA,0BAAA;EAAA,kBAAA;E7CsiJH;A6C7hJD;EEvFE,2BAAA;E/CunJD;A+CpnJC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9CukJH;A6CjiJD;EE3FE,2BAAA;E/C+nJD;A+C5nJC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9C+kJH;A6CriJD;EE/FE,2BAAA;E/CuoJD;A+CpoJC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9CulJH;A6CziJD;EEnGE,2BAAA;E/C+oJD;A+C5oJC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9C+lJH;AgD9oJD;;EAEE,kBAAA;EACA,SAAA;EhDgpJD;AgD5oJD;;EAEE,kBAAA;EhD8oJD;AgD5oJD;EACE,eAAA;EhD8oJD;AgD1oJD;EACE,gBAAA;EhD4oJD;AgDxoJD;EACE,iBAAA;EhD0oJD;AgDnoJD;EAEI,oBAAA;EhDooJH;AgDtoJD;EAKI,mBAAA;EhDooJH;AgD3nJD;EACE,iBAAA;EACA,kBAAA;EhD6nJD;AiD1qJD;EAEE,qBAAA;EACA,iBAAA;EjD2qJD;AiDnqJD;EACE,oBAAA;EACA,gBAAA;EACA,oBAAA;EAEA,qBAAA;EACA,2BAAA;EACA,2BAAA;EjDoqJD;AiDjqJC;ErB3BA,8BAAA;EACC,6BAAA;E5B+rJF;AiDlqJC;EACE,kBAAA;ErBvBF,iCAAA;EACC,gCAAA;E5B4rJF;AiDprJD;EAoBI,cAAA;EjDmqJH;AiDvrJD;EAuBI,mBAAA;EjDmqJH;AiDzpJD;EACE,gBAAA;EjD2pJD;AiD5pJD;EAII,gBAAA;EjD2pJH;AiDvpJC;;EAEE,uBAAA;EACA,gBAAA;EACA,2BAAA;EjDypJH;AiDnpJC;;;EAGE,2BAAA;EACA,gBAAA;EjDqpJH;AiDzpJC;;;EAQI,gBAAA;EjDspJL;AiD9pJC;;;EAWI,gBAAA;EjDwpJL;AiDnpJC;;;EAGE,YAAA;EACA,gBAAA;EACA,2BAAA;EACA,uBAAA;EjDqpJH;AiD3pJC;;;;;;;;;EAYI,gBAAA;EjD0pJL;AiDtqJC;;;EAeI,gBAAA;EjD4pJL;AkD/vJC;EACE,gBAAA;EACA,2BAAA;ElDiwJH;AkD/vJG;EACE,gBAAA;ElDiwJL;AkDlwJG;EAII,gBAAA;ElDiwJP;AkD9vJK;;EAEE,gBAAA;EACA,2BAAA;ElDgwJP;AkD9vJK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElDgwJP;AkDrxJC;EACE,gBAAA;EACA,2BAAA;ElDuxJH;AkDrxJG;EACE,gBAAA;ElDuxJL;AkDxxJG;EAII,gBAAA;ElDuxJP;AkDpxJK;;EAEE,gBAAA;EACA,2BAAA;ElDsxJP;AkDpxJK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElDsxJP;AkD3yJC;EACE,gBAAA;EACA,2BAAA;ElD6yJH;AkD3yJG;EACE,gBAAA;ElD6yJL;AkD9yJG;EAII,gBAAA;ElD6yJP;AkD1yJK;;EAEE,gBAAA;EACA,2BAAA;ElD4yJP;AkD1yJK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElD4yJP;AkDj0JC;EACE,gBAAA;EACA,2BAAA;ElDm0JH;AkDj0JG;EACE,gBAAA;ElDm0JL;AkDp0JG;EAII,gBAAA;ElDm0JP;AkDh0JK;;EAEE,gBAAA;EACA,2BAAA;ElDk0JP;AkDh0JK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElDk0JP;AiD/tJD;EACE,eAAA;EACA,oBAAA;EjDiuJD;AiD/tJD;EACE,kBAAA;EACA,kBAAA;EjDiuJD;AmD51JD;EACE,qBAAA;EACA,2BAAA;EACA,+BAAA;EACA,oBAAA;E9C0DA,mDAAA;EACQ,2CAAA;ELqyJT;AmD31JD;EACE,eAAA;EnD61JD;AmDx1JD;EACE,oBAAA;EACA,sCAAA;EvBpBA,8BAAA;EACC,6BAAA;E5B+2JF;AmD91JD;EAMI,gBAAA;EnD21JH;AmDt1JD;EACE,eAAA;EACA,kBAAA;EACA,iBAAA;EACA,gBAAA;EnDw1JD;AmD51JD;EAOI,gBAAA;EnDw1JH;AmDn1JD;EACE,oBAAA;EACA,2BAAA;EACA,+BAAA;EvBpCA,iCAAA;EACC,gCAAA;E5B03JF;AmD70JD;EAEI,kBAAA;EnD80JH;AmDh1JD;EAKM,qBAAA;EACA,kBAAA;EnD80JL;AmD10JG;EAEI,eAAA;EvBlEN,8BAAA;EACC,6BAAA;E5B84JF;AmDx0JG;EAEI,kBAAA;EvBjEN,iCAAA;EACC,gCAAA;E5B24JF;AmDp0JD;EAEI,qBAAA;EnDq0JH;AmDl0JD;EACE,qBAAA;EnDo0JD;AmD5zJD;;;EAII,kBAAA;EnD6zJH;AmDj0JD;;EvB9FE,8BAAA;EACC,6BAAA;E5Bm6JF;AmDt0JD;;;;;;;;EAgBU,6BAAA;EnDg0JT;AmDh1JD;;;;;;;;EAoBU,8BAAA;EnDs0JT;AmD11JD;;EvBtFE,iCAAA;EACC,gCAAA;E5Bo7JF;AmD/1JD;;;;;;;;EAmCU,gCAAA;EnDs0JT;AmDz2JD;;;;;;;;EAuCU,iCAAA;EnD40JT;AmDn3JD;;EA8CI,+BAAA;EnDy0JH;AmDv3JD;;EAkDI,eAAA;EnDy0JH;AmD33JD;;EAsDI,WAAA;EnDy0JH;AmD/3JD;;;;;;;;;;;;EA6DU,gBAAA;EnDg1JT;AmD74JD;;;;;;;;;;;;EAiEU,iBAAA;EnD01JT;AmD35JD;;;;;;;;EA0EU,kBAAA;EnD21JT;AmDr6JD;;;;;;;;EAmFU,kBAAA;EnD41JT;AmD/6JD;EAyFI,WAAA;EACA,kBAAA;EnDy1JH;AmD/0JD;EACE,qBAAA;EnDi1JD;AmDl1JD;EAKI,kBAAA;EACA,oBAAA;EnDg1JH;AmDt1JD;EAQM,iBAAA;EnDi1JL;AmDz1JD;EAaI,kBAAA;EnD+0JH;AmD51JD;EAeM,+BAAA;EnDg1JL;AmD/1JD;EAmBI,eAAA;EnD+0JH;AmDl2JD;EAqBM,kCAAA;EnDg1JL;AmDz0JD;EC9NE,uBAAA;EpD0iKD;AoDxiKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpD0iKH;AoD7iKC;EAMI,2BAAA;EpD0iKL;AoDhjKC;EASI,gBAAA;EACA,2BAAA;EpD0iKL;AoDviKC;EAEI,8BAAA;EpDwiKL;AmDx1JD;ECjOE,uBAAA;EpD4jKD;AoD1jKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpD4jKH;AoD/jKC;EAMI,2BAAA;EpD4jKL;AoDlkKC;EASI,gBAAA;EACA,2BAAA;EpD4jKL;AoDzjKC;EAEI,8BAAA;EpD0jKL;AmDv2JD;ECpOE,uBAAA;EpD8kKD;AoD5kKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpD8kKH;AoDjlKC;EAMI,2BAAA;EpD8kKL;AoDplKC;EASI,gBAAA;EACA,2BAAA;EpD8kKL;AoD3kKC;EAEI,8BAAA;EpD4kKL;AmDt3JD;ECvOE,uBAAA;EpDgmKD;AoD9lKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDgmKH;AoDnmKC;EAMI,2BAAA;EpDgmKL;AoDtmKC;EASI,gBAAA;EACA,2BAAA;EpDgmKL;AoD7lKC;EAEI,8BAAA;EpD8lKL;AmDr4JD;EC1OE,uBAAA;EpDknKD;AoDhnKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDknKH;AoDrnKC;EAMI,2BAAA;EpDknKL;AoDxnKC;EASI,gBAAA;EACA,2BAAA;EpDknKL;AoD/mKC;EAEI,8BAAA;EpDgnKL;AmDp5JD;EC7OE,uBAAA;EpDooKD;AoDloKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDooKH;AoDvoKC;EAMI,2BAAA;EpDooKL;AoD1oKC;EASI,gBAAA;EACA,2BAAA;EpDooKL;AoDjoKC;EAEI,8BAAA;EpDkoKL;AqDlpKD;EACE,oBAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,kBAAA;ErDopKD;AqDzpKD;;;;EAWI,oBAAA;EACA,QAAA;EACA,SAAA;EACA,WAAA;EACA,cAAA;EACA,aAAA;EACA,WAAA;ErDopKH;AqDhpKC;EACE,wBAAA;ErDkpKH;AqD9oKC;EACE,qBAAA;ErDgpKH;AsDzqKD;EACE,kBAAA;EACA,eAAA;EACA,qBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;EjDwDA,yDAAA;EACQ,iDAAA;ELonKT;AsDnrKD;EASI,oBAAA;EACA,mCAAA;EtD6qKH;AsDxqKD;EACE,eAAA;EACA,oBAAA;EtD0qKD;AsDxqKD;EACE,cAAA;EACA,oBAAA;EtD0qKD;AuDhsKD;EACE,cAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBAAA;EACA,8BAAA;EjCRA,cAAA;EAGA,2BAAA;EtBysKD;AuDjsKC;;EAEE,gBAAA;EACA,uBAAA;EACA,iBAAA;EjCfF,cAAA;EAGA,2BAAA;EtBitKD;AuD9rKC;EACE,YAAA;EACA,iBAAA;EACA,yBAAA;EACA,WAAA;EACA,0BAAA;EvDgsKH;AwDptKD;EACE,kBAAA;ExDstKD;AwDltKD;EACE,eAAA;EACA,kBAAA;EACA,iBAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EACA,mCAAA;EAIA,YAAA;ExDitKD;AwD9sKC;EnDkHA,4CAAA;EACQ,uCAAA;EAAA,oCAAA;EA8DR,qDAAA;EAEK,2CAAA;EACG,qCAAA;ELkiKT;AwDltKC;EnD8GA,yCAAA;EACQ,oCAAA;EAAA,iCAAA;ELumKT;AwDptKD;EACE,oBAAA;EACA,kBAAA;ExDstKD;AwDltKD;EACE,oBAAA;EACA,aAAA;EACA,cAAA;ExDotKD;AwDhtKD;EACE,oBAAA;EACA,2BAAA;EACA,2BAAA;EACA,sCAAA;EACA,oBAAA;EnDaA,kDAAA;EACQ,0CAAA;EmDZR,sCAAA;EAAA,8BAAA;EAEA,YAAA;ExDktKD;AwD9sKD;EACE,iBAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EACA,2BAAA;ExDgtKD;AwD9sKC;ElCrEA,YAAA;EAGA,0BAAA;EtBoxKD;AwDjtKC;ElCtEA,cAAA;EAGA,2BAAA;EtBwxKD;AwDhtKD;EACE,eAAA;EACA,kCAAA;EACA,2BAAA;ExDktKD;AwD/sKD;EACE,kBAAA;ExDitKD;AwD7sKD;EACE,WAAA;EACA,yBAAA;ExD+sKD;AwD1sKD;EACE,oBAAA;EACA,eAAA;ExD4sKD;AwDxsKD;EACE,eAAA;EACA,mBAAA;EACA,+BAAA;ExD0sKD;AwD7sKD;EAQI,kBAAA;EACA,kBAAA;ExDwsKH;AwDjtKD;EAaI,mBAAA;ExDusKH;AwDptKD;EAiBI,gBAAA;ExDssKH;AwDjsKD;EACE,oBAAA;EACA,cAAA;EACA,aAAA;EACA,cAAA;EACA,kBAAA;ExDmsKD;AwDjrKD;EAZE;IACE,cAAA;IACA,mBAAA;IxDgsKD;EwD9rKD;InDvEA,mDAAA;IACQ,2CAAA;ILwwKP;EwD7rKD;IAAY,cAAA;IxDgsKX;EACF;AwD3rKD;EAFE;IAAY,cAAA;IxDisKX;EACF;AyDh1KD;EACE,oBAAA;EACA,eAAA;EACA,gBAAA;EACA,qBAAA;EACA,iBAAA;EACA,kBAAA;EnCTA,YAAA;EAGA,0BAAA;EtB01KD;AyDj1KC;EnCZA,cAAA;EAGA,2BAAA;EtB81KD;AyDp1KC;EAAW,kBAAA;EAAmB,gBAAA;EzDw1K/B;AyDv1KC;EAAW,kBAAA;EAAmB,gBAAA;EzD21K/B;AyD11KC;EAAW,iBAAA;EAAmB,gBAAA;EzD81K/B;AyD71KC;EAAW,mBAAA;EAAmB,gBAAA;EzDi2K/B;AyD71KD;EACE,kBAAA;EACA,kBAAA;EACA,gBAAA;EACA,oBAAA;EACA,uBAAA;EACA,2BAAA;EACA,oBAAA;EzD+1KD;AyD31KD;EACE,oBAAA;EACA,UAAA;EACA,WAAA;EACA,2BAAA;EACA,qBAAA;EzD61KD;AyD11KC;EACE,WAAA;EACA,WAAA;EACA,mBAAA;EACA,yBAAA;EACA,2BAAA;EzD41KH;AyD11KC;EACE,WAAA;EACA,WAAA;EACA,yBAAA;EACA,2BAAA;EzD41KH;AyD11KC;EACE,WAAA;EACA,YAAA;EACA,yBAAA;EACA,2BAAA;EzD41KH;AyD11KC;EACE,UAAA;EACA,SAAA;EACA,kBAAA;EACA,6BAAA;EACA,6BAAA;EzD41KH;AyD11KC;EACE,UAAA;EACA,UAAA;EACA,kBAAA;EACA,6BAAA;EACA,4BAAA;EzD41KH;AyD11KC;EACE,QAAA;EACA,WAAA;EACA,mBAAA;EACA,yBAAA;EACA,8BAAA;EzD41KH;AyD11KC;EACE,QAAA;EACA,WAAA;EACA,yBAAA;EACA,8BAAA;EzD41KH;AyD11KC;EACE,QAAA;EACA,YAAA;EACA,yBAAA;EACA,8BAAA;EzD41KH;A0Dn7KD;EACE,oBAAA;EACA,QAAA;EACA,SAAA;EACA,eAAA;EACA,eAAA;EACA,kBAAA;EACA,cAAA;EACA,kBAAA;EACA,2BAAA;EACA,sCAAA;EAAA,8BAAA;EACA,2BAAA;EACA,sCAAA;EACA,oBAAA;ErDkDA,mDAAA;EACQ,2CAAA;EqD/CR,qBAAA;E1Do7KD;A0Dj7KC;EAAY,mBAAA;E1Do7Kb;A0Dn7KC;EAAY,mBAAA;E1Ds7Kb;A0Dr7KC;EAAY,kBAAA;E1Dw7Kb;A0Dv7KC;EAAY,oBAAA;E1D07Kb;A0Dv7KD;EACE,WAAA;EACA,mBAAA;EACA,iBAAA;EACA,qBAAA;EACA,mBAAA;EACA,2BAAA;EACA,kCAAA;EACA,4BAAA;E1Dy7KD;A0Dt7KD;EACE,mBAAA;E1Dw7KD;A0Dh7KC;;EAEE,oBAAA;EACA,gBAAA;EACA,UAAA;EACA,WAAA;EACA,2BAAA;EACA,qBAAA;E1Dk7KH;A0D/6KD;EACE,oBAAA;E1Di7KD;A0D/6KD;EACE,oBAAA;EACA,aAAA;E1Di7KD;A0D76KC;EACE,WAAA;EACA,oBAAA;EACA,wBAAA;EACA,2BAAA;EACA,uCAAA;EACA,eAAA;E1D+6KH;A0D96KG;EACE,cAAA;EACA,aAAA;EACA,oBAAA;EACA,wBAAA;EACA,2BAAA;E1Dg7KL;A0D76KC;EACE,UAAA;EACA,aAAA;EACA,mBAAA;EACA,sBAAA;EACA,6BAAA;EACA,yCAAA;E1D+6KH;A0D96KG;EACE,cAAA;EACA,WAAA;EACA,eAAA;EACA,sBAAA;EACA,6BAAA;E1Dg7KL;A0D76KC;EACE,WAAA;EACA,oBAAA;EACA,qBAAA;EACA,8BAAA;EACA,0CAAA;EACA,YAAA;E1D+6KH;A0D96KG;EACE,cAAA;EACA,UAAA;EACA,oBAAA;EACA,qBAAA;EACA,8BAAA;E1Dg7KL;A0D56KC;EACE,UAAA;EACA,cAAA;EACA,mBAAA;EACA,uBAAA;EACA,4BAAA;EACA,wCAAA;E1D86KH;A0D76KG;EACE,cAAA;EACA,YAAA;EACA,uBAAA;EACA,4BAAA;EACA,eAAA;E1D+6KL;A2DziLD;EACE,oBAAA;E3D2iLD;A2DxiLD;EACE,oBAAA;EACA,kBAAA;EACA,aAAA;E3D0iLD;A2D7iLD;EAMI,eAAA;EACA,oBAAA;EtD0KF,2CAAA;EACK,sCAAA;EACG,mCAAA;ELi4KT;A2DpjLD;;EAcM,gBAAA;E3D0iLL;A2DxjLD;;;EAqBI,gBAAA;E3DwiLH;A2D7jLD;EAyBI,SAAA;E3DuiLH;A2DhkLD;;EA8BI,oBAAA;EACA,QAAA;EACA,aAAA;E3DsiLH;A2DtkLD;EAoCI,YAAA;E3DqiLH;A2DzkLD;EAuCI,aAAA;E3DqiLH;A2D5kLD;;EA2CI,SAAA;E3DqiLH;A2DhlLD;EA+CI,aAAA;E3DoiLH;A2DnlLD;EAkDI,YAAA;E3DoiLH;A2D5hLD;EACE,oBAAA;EACA,QAAA;EACA,SAAA;EACA,WAAA;EACA,YAAA;ErCtEA,cAAA;EAGA,2BAAA;EqCqEA,iBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2CAAA;E3D+hLD;A2D1hLC;Eb1EE,oGAAA;EACA,+FAAA;EACA,sHAAA;EAAA,gGAAA;EACA,6BAAA;EACA,wHAAA;E9CumLH;A2D9hLC;EACE,YAAA;EACA,UAAA;Eb/EA,oGAAA;EACA,+FAAA;EACA,sHAAA;EAAA,gGAAA;EACA,6BAAA;EACA,wHAAA;E9CgnLH;A2DhiLC;;EAEE,YAAA;EACA,gBAAA;EACA,uBAAA;ErC9FF,cAAA;EAGA,2BAAA;EtB+nLD;A2DjkLD;;;;EAsCI,oBAAA;EACA,UAAA;EACA,YAAA;EACA,uBAAA;E3DiiLH;A2D1kLD;;EA6CI,WAAA;EACA,oBAAA;E3DiiLH;A2D/kLD;;EAkDI,YAAA;EACA,qBAAA;E3DiiLH;A2DplLD;;EAuDI,aAAA;EACA,cAAA;EACA,mBAAA;EACA,oBAAA;E3DiiLH;A2D5hLG;EACE,kBAAA;E3D8hLL;A2D1hLG;EACE,kBAAA;E3D4hLL;A2DlhLD;EACE,oBAAA;EACA,cAAA;EACA,WAAA;EACA,aAAA;EACA,YAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;E3DohLD;A2D7hLD;EAYI,uBAAA;EACA,aAAA;EACA,cAAA;EACA,aAAA;EACA,qBAAA;EACA,2BAAA;EACA,qBAAA;EACA,iBAAA;EAUA,2BAAA;EACA,oCAAA;E3D2gLH;A2DziLD;EAiCI,WAAA;EACA,aAAA;EACA,cAAA;EACA,2BAAA;E3D2gLH;A2DpgLD;EACE,oBAAA;EACA,WAAA;EACA,YAAA;EACA,cAAA;EACA,aAAA;EACA,mBAAA;EACA,sBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2CAAA;E3DsgLD;A2DrgLC;EACE,mBAAA;E3DugLH;A2D99KD;EAhCE;;;;IAKI,aAAA;IACA,cAAA;IACA,mBAAA;IACA,iBAAA;I3DggLH;E2DxgLD;;IAYI,oBAAA;I3DggLH;E2D5gLD;;IAgBI,qBAAA;I3DggLH;E2D3/KD;IACE,WAAA;IACA,YAAA;IACA,sBAAA;I3D6/KD;E2Dz/KD;IACE,cAAA;I3D2/KD;EACF;A4D/tLC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEE,cAAA;EACA,gBAAA;E5D6vLH;A4D3vLC;;;;;;;;;;;;;;;EACE,aAAA;E5D2wLH;AiCnxLD;E4BRE,gBAAA;EACA,mBAAA;EACA,oBAAA;E7D8xLD;AiCrxLD;EACE,yBAAA;EjCuxLD;AiCrxLD;EACE,wBAAA;EjCuxLD;AiC/wLD;EACE,0BAAA;EjCixLD;AiC/wLD;EACE,2BAAA;EjCixLD;AiC/wLD;EACE,oBAAA;EjCixLD;AiC/wLD;E6BzBE,aAAA;EACA,oBAAA;EACA,mBAAA;EACA,+BAAA;EACA,WAAA;E9D2yLD;AiC7wLD;EACE,0BAAA;EACA,+BAAA;EjC+wLD;AiCxwLD;EACE,iBAAA;E5B2FA,yCAAA;EACQ,oCAAA;EAAA,iCAAA;ELgrLT;A+D9yLD;EACE,qBAAA;E/DgzLD;A+D1yLD;;;;ECdE,0BAAA;EhE8zLD;A+DzyLD;;;;;;;;;;;;EAYE,0BAAA;E/D2yLD;A+DpyLD;EAAA;IChDE,2BAAA;IhEw1LC;EgEv1LD;IAAU,gBAAA;IhE01LT;EgEz1LD;IAAU,+BAAA;IhE41LT;EgE31LD;;IACU,gCAAA;IhE81LT;EACF;A+D9yLD;EAAA;IAFI,2BAAA;I/DozLD;EACF;A+D9yLD;EAAA;IAFI,4BAAA;I/DozLD;EACF;A+D9yLD;EAAA;IAFI,kCAAA;I/DozLD;EACF;A+D7yLD;EAAA;ICrEE,2BAAA;IhEs3LC;EgEr3LD;IAAU,gBAAA;IhEw3LT;EgEv3LD;IAAU,+BAAA;IhE03LT;EgEz3LD;;IACU,gCAAA;IhE43LT;EACF;A+DvzLD;EAAA;IAFI,2BAAA;I/D6zLD;EACF;A+DvzLD;EAAA;IAFI,4BAAA;I/D6zLD;EACF;A+DvzLD;EAAA;IAFI,kCAAA;I/D6zLD;EACF;A+DtzLD;EAAA;IC1FE,2BAAA;IhEo5LC;EgEn5LD;IAAU,gBAAA;IhEs5LT;EgEr5LD;IAAU,+BAAA;IhEw5LT;EgEv5LD;;IACU,gCAAA;IhE05LT;EACF;A+Dh0LD;EAAA;IAFI,2BAAA;I/Ds0LD;EACF;A+Dh0LD;EAAA;IAFI,4BAAA;I/Ds0LD;EACF;A+Dh0LD;EAAA;IAFI,kCAAA;I/Ds0LD;EACF;A+D/zLD;EAAA;IC/GE,2BAAA;IhEk7LC;EgEj7LD;IAAU,gBAAA;IhEo7LT;EgEn7LD;IAAU,+BAAA;IhEs7LT;EgEr7LD;;IACU,gCAAA;IhEw7LT;EACF;A+Dz0LD;EAAA;IAFI,2BAAA;I/D+0LD;EACF;A+Dz0LD;EAAA;IAFI,4BAAA;I/D+0LD;EACF;A+Dz0LD;EAAA;IAFI,kCAAA;I/D+0LD;EACF;A+Dx0LD;EAAA;IC5HE,0BAAA;IhEw8LC;EACF;A+Dx0LD;EAAA;ICjIE,0BAAA;IhE68LC;EACF;A+Dx0LD;EAAA;ICtIE,0BAAA;IhEk9LC;EACF;A+Dx0LD;EAAA;IC3IE,0BAAA;IhEu9LC;EACF;A+Dr0LD;ECnJE,0BAAA;EhE29LD;A+Dl0LD;EAAA;ICjKE,2BAAA;IhEu+LC;EgEt+LD;IAAU,gBAAA;IhEy+LT;EgEx+LD;IAAU,+BAAA;IhE2+LT;EgE1+LD;;IACU,gCAAA;IhE6+LT;EACF;A+Dh1LD;EACE,0BAAA;E/Dk1LD;A+D70LD;EAAA;IAFI,2BAAA;I/Dm1LD;EACF;A+Dj1LD;EACE,0BAAA;E/Dm1LD;A+D90LD;EAAA;IAFI,4BAAA;I/Do1LD;EACF;A+Dl1LD;EACE,0BAAA;E/Do1LD;A+D/0LD;EAAA;IAFI,kCAAA;I/Dq1LD;EACF;A+D90LD;EAAA;ICpLE,0BAAA;IhEsgMC;EACF","sourcesContent":[null,"/*! normalize.css v3.0.1 | MIT License | git.io/normalize */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS text size adjust after orientation change, without disabling\n// user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background: transparent;\n}\n\n//\n// Improve readability when focused and also mouse hovered in all browsers.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// Address styling not present in IE 8/9/10/11, Safari, and Chrome.\n//\n\nabbr[title] {\n border-bottom: 1px dotted;\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome\n// (include `-moz` to future-proof).\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n -moz-box-sizing: content-box;\n -webkit-box-sizing: content-box; // 2\n box-sizing: content-box;\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","//\n// Basic print styles\n// --------------------------------------------------\n// Source: https://github.com/h5bp/html5-boilerplate/blob/master/css/main.css\n\n@media print {\n\n * {\n text-shadow: none !important;\n color: #000 !important; // Black prints faster: h5bp.com/s\n background: transparent !important;\n box-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links for images, or javascript/internal links\n a[href^=\"javascript:\"]:after,\n a[href^=\"#\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245\n // Once fixed, we can just straight up remove this.\n select {\n background: #fff !important;\n }\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .table {\n td,\n th {\n background-color: #fff !important;\n }\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('@{icon-font-path}@{icon-font-name}.eot');\n src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'),\n url('@{icon-font-path}@{icon-font-name}.woff') format('woff'),\n url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'),\n url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg');\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\2a\"; } }\n.glyphicon-plus { &:before { content: \"\\2b\"; } }\n.glyphicon-euro { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: underline;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content/\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0,0,0,0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They will be removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n &::-moz-placeholder { color: @color; // Firefox\n opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n width: 100% \\9; // Force IE10 and below to size SVG images correctly\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: normal;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\n// Undo browser default styling\ncite {\n font-style: normal;\n}\n\nmark,\n.mark {\n background-color: @state-warning-bg;\n padding: .2em;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @grid-float-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: '\\2014 \\00A0'; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n text-align: right;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: ''; }\n &:after {\n content: '\\00A0 \\2014'; // nbsp, em dash\n }\n }\n}\n\n// Quotes\nblockquote:before,\nblockquote:after {\n content: \"\";\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n word-break: break-all;\n word-wrap: break-word;\n color: @pre-color;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n margin-right: auto;\n margin-left: auto;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: (@gutter / -2);\n margin-right: (@gutter / -2);\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n}\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-child(odd) {\n > td,\n > th {\n background-color: @table-bg-accent;\n }\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n > td,\n > th {\n background-color: @table-bg-hover;\n }\n }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-column;\n}\ntable {\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-cell;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * 0.75);\n overflow-y: hidden;\n overflow-x: auto;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n -webkit-overflow-scrolling: touch;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n}\n\n// Set the height of file controls to match text inputs\ninput[type=\"file\"] {\n display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n .tab-focus();\n}\n\n// Adjust output element\noutput {\n display: block;\n padding-top: (@padding-base-vertical + 1);\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n display: block;\n width: 100%;\n height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n background-color: @input-bg;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid @input-border;\n border-radius: @input-border-radius;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n // Customize the `:focus` state to imitate native WebKit styles.\n .form-control-focus();\n\n // Placeholder\n .placeholder();\n\n // Disabled and read-only inputs\n //\n // HTML5 says that controls under a fieldset > legend:first-child won't be\n // disabled if the fieldset is disabled. Due to implementation difficulty, we\n // don't honor that edge case; we style them as disabled anyway.\n &[disabled],\n &[readonly],\n fieldset[disabled] & {\n cursor: not-allowed;\n background-color: @input-bg-disabled;\n opacity: 1; // iOS fix for unreadable disabled content\n }\n\n // Reset height for `textarea`s\n textarea& {\n height: auto;\n }\n}\n\n\n// Search inputs in iOS\n//\n// This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n\n\n// Special styles for iOS temporal inputs\n//\n// In Mobile Safari, setting `display: block` on temporal inputs causes the\n// text within the input to become vertically misaligned.\n// As a workaround, we set a pixel line-height that matches the\n// given height of the input. Since this fucks up everything else, we have to\n// appropriately reset it for Internet Explorer and the size variations.\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n line-height: @input-height-base;\n // IE8+ misaligns the text within date inputs, so we reset\n line-height: @line-height-base ~\"\\0\";\n\n &.input-sm {\n line-height: @input-height-small;\n }\n &.input-lg {\n line-height: @input-height-large;\n }\n}\n\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n margin-bottom: 15px;\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.radio,\n.checkbox {\n position: relative;\n display: block;\n min-height: @line-height-computed; // clear the floating input if there is no label text\n margin-top: 10px;\n margin-bottom: 10px;\n\n label {\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n }\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing\n}\n\n// Radios and checkboxes on same line\n.radio-inline,\n.checkbox-inline {\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px; // space out consecutive inline controls\n}\n\n// Apply same disabled cursor tweak as for inputs\n// Some special care is needed because
'; +``` + +## Additional naming convention standards + +### General naming conventions + +* Avoid underscores and numbers in names. +* Variables or methods should have names that accurately describe their purpose or behavior. +* Object methods or variables that are declared `private` or `protected` should start with an underscore(`_`). + +### Functions and methods + +* Class method names should start with an English verb in its infinitive form that describes the method. +* Names for accessors for instance or static variables should always have the `get` or `set` prefix. +* In [design pattern](https://glossary.magento.com/design-pattern) classes, implementation method names should contain the pattern name where practical to provide better behavior description. +* Methods that return status flags or Boolean values should have the `has` or `is` prefix. + +### Variables and properties + +* Do not use short variable names such as `i` or `n` except in small loop contexts +* If a loop contains more than 20 lines of code, the index variables should have more descriptive names. + +## Additional coding construct standards + +### Binary and ternary operators + +Always put the operator on the preceding line to avoid implicit semi-colon insertion issues. + +### Custom `toString()` method + +This method must always succeed without side effects. + +### Function declarations within blocks + +Use a variable initialized with a function expression to define a function within a block. + +```javascript +// Wrong +if (x) { + function foo() {} +} + +// Correct +if (x) { + var foo = function() {} +} +``` + +### Exceptions and custom exceptions + +You cannot avoid exceptions if you are doing something non-trivial (using an application development framework, and so on). + +Without custom exceptions, returning error information from a function that also returns a value can be tricky, not to mention inelegant. +Bad solutions include passing in a reference type to hold error information or always returning Objects with a potential error member. + +These basically amount to a primitive [exception](https://glossary.magento.com/exception) handling hack. +Feel free to use custom exceptions when appropriate. + +### Standard features {#standard-features} + +For maximum portability and compatibility, use standard features whenever possible. + +For example, `string.charAt(3)` instead of `string[3]`, and element access with DOM functions instead of using an application-specific shorthand. + +### Method definitions + +There are several ways to attach methods and properties to a constructor, but the preferred style is: + +```javascript +Foo.prototype.bar = function() { + // ... +}; +``` + +Do not use: + +```javascript +Foo.prototype = { + bar: function() { + // ... + }, + circle: function() { + // ... + } +}; + +``` + +Assignment operations to constructor prototypes creating temporal coupling and sometimes other unwanted side effects. + +### Closures + +A closure keeps a pointer to its enclosing scope, so attaching a closure to a DOM element can create a circular reference and thus, a memory leak. + +```javascript +// Wrong +function foo(element, a, b) { + element.onclick = function() { + // uses a and b + }; +} +``` + +The function closure keeps references to elements "a" and "b" even if it never uses them. + +Because elements also keep references to the closure, it is a cycle that will not be cleaned up by garbage collection. +In these situations, the code can be structured as follows: + +```javascript +// Correct +function foo(element, a, b) { + element.onclick = bar(a, b); +} + +function bar(a, b) { + return function() { + // uses a and b + } +} +``` + +## Additional general standards + +### Array and object initializers + +Single-line array and object initializers are allowed when they fit on a line as follows: + +```javascript + var arr = [1, 2, 3]; // No space after [ or before ]. + var obj = {a: 1, b: 2, c: 3}; // No space after { or before }. +``` + +Long identifiers or values present problems for aligned initialization lists, so always prefer non-aligned initialization. + +For example: + +```javascript +Object.prototype = { + a: 0, + b: 1, + lengthyName: 2 +}; +``` + +### Associative arrays + +Use `Object` instead of `Array` for associative arrays. + +### Deferred initialization + +Use deferred initialization when it is not possible to initialize variables at the point of declaration. + +### Explicit scope + +Use explicit scope to increase code portability and clarity. + +### Built-in objects + +Modifying built-in like `Object.prototype` and `Array.prototype` is strictly forbidden. + +Modifying other built-ins like `Function.prototype` is less dangerous but leads to debugging issue in production. + +### Variable declarations + +Declare a variable with `var` wherever possible to avoid overwriting existing global values. + +Using only one var per scope promotes readability. + +```javascript +var foo = 'bar', + num = 1, + arr = [1, 2, 3]; +``` + +[jquery]: https://jquery.com/ +[jquery-widgets]: https://api.jqueryui.com/category/widgets +[jquery-widget-coding-standard]: {{ page.baseurl }}/coding-standards/code-standard-jquery-widgets.html +[eslint]: https://eslint.org/ +[eslint-rules]: https://github.com/magento/magento-coding-standard/blob/develop/eslint/.eslintrc-magento diff --git a/src/guides/v2.3/coding-standards/code-standard-jquery-widgets.md b/src/guides/v2.3/coding-standards/code-standard-jquery-widgets.md new file mode 100644 index 00000000000..7bb255b6320 --- /dev/null +++ b/src/guides/v2.3/coding-standards/code-standard-jquery-widgets.md @@ -0,0 +1,355 @@ +--- +group: coding-standards +subgroup: 01_Coding standards +title: jQuery widget coding standard +landing-page: Coding standards +menu_title: jQuery widget coding standard +menu_order: 7 +functional_areas: + - Standards +--- + +In the Magento system, all jQuery UI widgets and interactions are built on a simple, reusable base---the [jQuery UI Widget Factory][jquery-ui-widget-factory]. + +The factory provides a flexible base for building complex, stateful plug-ins with a consistent [API](https://glossary.magento.com/api). +It is designed not only for plug-ins that are part of [jQuery](https://glossary.magento.com/jquery) UI, but for general usage by developers who want to create object-oriented components without reinventing common infrastructure. + +For more information, see the [jQuery Widget API documentation][jquery-ui-api-doc]. + +This standard is mandatory for Magento core developers and recommended for third-party [extension](https://glossary.magento.com/extension) developers. +Some parts of Magento code might not comply with the standard, but we are working to gradually improve this. + +Use [RFC 2119][rfc2119] to interpret the "must," "must not," "required," "shall," "shall not," "should," "should not," "recommended," "may," and "optional" keywords. + +## Naming conventions + +* [Widget](https://glossary.magento.com/widget) names must consist of one or more non-abbreviated English word and in camelcase format. + + ```javascript + (function($) { + $.widget('mage.accordion', $.ui.accordion, { + // ... My custom code ... + }); + ``` + +* Widget names should be verbose enough to fully describe their purpose and behavior. + + ```javascript + // Declaration of the frontend.advancedEventTrigger widget + (function($) { + "use strict"; + + $.widget('mage.advancedEventTrigger', $.ui.button, { + // ... My custom code ... + }); + }) (jQuery); + ``` + +## Instantiation and resources + +* Additional [JavaScript](https://glossary.magento.com/javascript) files used as a resources must be dynamically loaded using the `$.mage.components()` method and must not be included in the `` block. +* Use the `$.mage.components()` method to load additional JavaScript resource files not included in the `` block. +* You must use `$.mage.extend()` to extend an existing set of widget resources. +* You must instantiate widgets using the `data-mage-init` attribute. + You can use the `.mage()` [plug-in](https://glossary.magento.com/plug-in) to instantiate widgets that use callback methods. + + Benefits: + + * You leverage the benefits of `$.mage.extend()` and `$.mage.components()`. + * Using `data-mage-init` minimizes the inline JavaScript code footprint. + * You can modify widget initialization parameters. + + ```javascript + // Widget initialization using the data-mage-init attribute +
+ + // Widget initialization using the mage plug-in + (function($) { + $('selector').mage('dialog', { + close: function(e) { + $(this).dialog('destroy'); + } + }); + })(jQuery); + ``` + +* You can declare callback methods inline JavaScript but not methods and widgets. + + ```javascript + // Widget initialization and configuration + $('selector').mage('dialog', { + close: function(e) { + $(this).dialog('destroy'); + } + }); + + // Widget initialization and binding event handlers + $('selector').mage('dialog').on('dialogclose', { + $(this).dialog('destroy'); + }); + + // Extension for widget in a JavaScript file + $.widget('mage.dialog', $.ui.dialog, { + close: function() { + this.destroy(); + } + }); + + // Extension of widget resources + (function($) { + $.mage + .extend('dialog', 'dialog', + 'getViewFileUrl('Enterprise_\*Module\*::page/js/dialog.js') ?>') + })(jQuery); + ``` + +### Initializing a component on a selector + +There are two ways to initialize a component on a selector: + +* Initialize the component in the `data-mage-init` attribute: + + ```html +
+ ``` + +* Use a script type `text/x-magento-init` attribute: + + ```html + + ``` + +In these cases the path to the file is: + + `Vendor/Module/view/frontend/web/js/jsfilename.js` + + which contains your code: + + ```javascript + define(['uiComponent'], + function (Component) { + 'use strict'; + return Component.extend({ + initialize: function (config, node) { + // some code + } + }); + }); + ``` + +### Initializing a component on a selector with parameters + +When a component is initialized, it is also important to send parameters to it, which are normally determined dynamically in PHP. + +* `data-mage-init` + + ```html +
+ ``` + +* Using a script type `text/x-magento-init` attribute. For example: + + ```html + + ``` + +## Development standards + +* Widgets should comply with the [single responsibility principle][single-responsibility-principle]. + + Widgets should not have responsibilities not related to the [entity](https://glossary.magento.com/entity) described by the widget. + + ```javascript + // Widget "dialog" that is responsible + // only for opening content in an interactive overlay. + $.widget('mage.dialog', { + // Code logic + }); + + // Widget "validation" that is responsible + // only for validating the form fields. + $.widget('mage.validation', $.ui.sortable, { + // Code logic + }); + + $('selector') + .mage('dialog') + .find('form') + .mage('validation'); + ``` + +* Widget properties that modify the widget's behavior must be located in the widget's options to make them configurable and reusable. + + ```javascript + //Declaration of the backend.dialog widget + $.widget('mage.dialog', { + options: { + modal: false, + autoOpen: true, + // Additional widget options + }, + // Additional widget properties + }); + + // Initializing + $('selector').mage('dialog', { + modal: true, + autoOpen: false + }); + ``` + +* Widget communications must be handled by jQuery events + + ```html + + ... + + + + + + + .... +
+``` + +### Step 1: Define the layout blocks + +ExampleCorp [applies the Luma theme]({{ page.baseurl }}/frontend-dev-guide/themes/theme-apply.html). Using the approach described in [Locate templates, layouts, and styles]({{ page.baseurl }}/frontend-dev-guide/themes/debug-theme.html) they find out that the original block responsible for displaying the header links is defined in + +`/view/frontend/layout/default.xml`: + +```xml + + ... + + + header links + + + +``` + +(See [app/code/Magento/Theme/view/frontend/layout/default.xml]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Theme/view/frontend/layout/default.xml#L43-L47) on GitHub). + +Other modules use this block to add their specific links to the header using the [referenceBlock]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html#fedg_layout_xml-instruc_ex_ref) instruction. For example, see how links are added in the Customer module: [app/code/Magento/Customer/view/frontend/layout/default.xml#L10-L23]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Customer/view/frontend/layout/default.xml#L10-L23) + +The Luma theme [moves]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html#fedg_layout_xml-instruc_ex_mv) the `top.links` block to the new `customer` block in the extending layout file. + +`/Magento_Customer/layout/default.xml` + +```xml + + + ... + +... + +... +``` + +The links that should be in header, but outside the drop-down menu are added in the new `header.links` block (`/Magento_Theme/layout/default.xml`): + +```xml + + + + header links + + + +``` + +### Step 2: Define the templates + +Similar to the way they defined the layout on the previous step, ExampleCorp +defines the template which is used as the drop-down container : `/view/frontend/templates/account/customer.phtml`. + +```php +customerLoggedIn()): ?> +
  • + + + + + + getChildHtml()):?> +
    + getChildHtml();?> +
    + +
  • + +``` + +See [app/code/Magento/Customer/view/frontend/templates/account/customer.phtml]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Customer/view/frontend/templates/account/customer.phtml). + +### Step 3: Extend the base layout to add a block + +ExampleCorp needs to create a new block, say, `header.links`, in the `header.panel` container, to move the links there. As the links can be added to this list by different modules, it is better to add this block to the `default.xml` page configuration of the `Magento_Theme` module. + +So the following [extending]({{ page.baseurl }}/frontend-dev-guide/layouts/layout-extend.html) layout is added in the Orange theme: + +`app/design/frontend/ExampleCorp/orange/Magento_Theme/layout/default.xml` + +```xml + + + + + + + header links + + + + + +``` + +### Step 4: Move links + +To move the links to the `header.links` block, ExampleCorp adds an extending layout: + +`app/design/frontend/ExampleCorp/orange/Magento_Customer/layout/default.xml` + +```xml + + + + + + + + + + + + +``` + +Now the customer links look like following: + +![layout screen1] + +Clicking the **Change** button toggles the `active` CSS class: + +To add quick basic styling and visual behavior to the "dropdown" menu, ExampleCorp added [_extend.less]({{ page.baseurl }}/frontend-dev-guide/css-guide/css_quick_guide_approach.html#simple_extend) to their theme with the following customizations: + +* Redundant elements are hidden with CSS. +* The `.lib-dropdown()` mixin from [Magento UI library]({{ page.baseurl }}/frontend-dev-guide/css-topics/theme-ui-lib.html) was applied to the corresponding element. + +`app/design/frontend/ExampleCorp/orange/web/css/source/_extend.less` + +```css +// +// Common +// _____________________________________________ + +& when (@media-common = true) { + .header.panel .header.links { + .customer-welcome + .authorization-link { + display: none; + } + } +} + +// +// Mobile +// _____________________________________________ + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .customer-name, + .customer-welcome + .authorization-link { + display: none; + } +} + +// +// Desktop +// _____________________________________________ + +.media-width(@extremum, @break) when (@extremum = 'min') and (@break = @screen__m) { + .customer-welcome { + .lib-dropdown( + @_toggle-selector: ~'.action.switch', + @_options-selector: ~'.customer-menu .header.links', + @_dropdown-actions-padding: 0, + @_icon-font-text-hide: true, + @_icon-font-size: 22px, + @_icon-font-line-height: 22px, + @_dropdown-list-min-width: 160px, + @_dropdown-list-item-hover: transparent, + @_dropdown-list-pointer-position: right, + @_dropdown-list-position-right: 0 + ); + + li { + a { + .lib-link( + @_link-color: #333, + @_link-text-decoration: none, + @_link-color-visited: #333, + @_link-text-decoration-visited: none, + @_link-color-hover: #333, + @_link-text-decoration-hover: none, + @_link-color-active: #333, + @_link-text-decoration-active: none + ); + display: block; + line-height: 1.4; + padding: 8px; + } + } + } +} +``` + +As a result, the customer links look like following: + +![layout screen2] + +[layout transform]: {{site.baseurl}}/common/images/layout_transform21.png +[layout screen1]: {{site.baseurl}}/common/images/layout_screen221.png +[layout screen2]: {{site.baseurl}}/common/images/layout_screen321.png \ No newline at end of file diff --git a/src/guides/v2.3/frontend-dev-guide/layouts/layout-types.md b/src/guides/v2.3/frontend-dev-guide/layouts/layout-types.md new file mode 100644 index 00000000000..46cd81f5557 --- /dev/null +++ b/src/guides/v2.3/frontend-dev-guide/layouts/layout-types.md @@ -0,0 +1,492 @@ +--- +group: frontend-developer-guide +title: Layout file types +functional_areas: + - Frontend +--- + +## What's in this topic + +For a particular page, its layout is defined by two major layout components: *page layout* file and *page configuration* file. + +A page layout file defines the page wireframe, for example, one-column layout. Technically page layout is an .xml file defining the structure inside the `` section of the HTML page markup. Page layouts feature only [containers]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html#fedg_layout_xml-instruc_ex_cont). +All page layouts used for page rendering should be declared in the page layout declaration file. + +Page configuration is also an .xml file. It defines the detailed structure (page header, footer, etc.), contents and page meta information, including the page layout used. Page configuration features both main elements, [blocks]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html#fedg_layout_xml-instruc_ex_block) and [containers]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html#fedg_layout_xml-instruc_ex_cont). + +We also distinguish the third type of layout files, *generic layouts*. They are .xml files which define the contents and detailed structure inside the `` section of the HTML page markup. These files are used for pages returned by AJAX requests, emails, HTML snippets and so on. + +This article gives a comprehensive description of each layout file type. + +## Page layout {#layout-types-page} + +Page layout declares the wireframe of a page inside the `` section. For example, one-column layout or two-column layout. + +Allowed layout instructions: + +* `` +* `` +* [``]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html#fedg_layout_xml-instruc_ex_cont) +* [``]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html#fedg_layout_xml-instruc_ex_ref) +* [``]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html#fedg_layout_xml-instruc_ex_mv) +* [``]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html#fedg_layout_xml-instruc_ex_upd) + +Sample page layout: + +`/view/frontend/page_layout/2columns-left.xml` + +```xml + + + + + + + + + + + +``` + +### Page layout files conventional location {#layout-types-page-conv} + +Conventionally page layouts must be located as follows: + +* Module page layouts: `/view/frontend/page_layout` +* Theme page layouts: `/_/page_layout` + +### Page layouts declaration {#layout-types-page-dec} + +To be able to use a layout for actual page rendering, you need to declare it in `layouts.xml`. + +Conventionally layout declaration file can be located in one of the following locations: + +* Module layout declarations: `/view/frontend/layouts.xml` +* Theme layout declaration: `/_/layouts.xml` + +Declare a layout file using the `` instruction, for which specify the following: + +* ``. For example, the `2columns-left.xml` page layout is declared like following: `` +* `` + +Sample page layout declaration file: `/view/frontend/layouts.xml` + +```xml + + + + + + + + + + + + + + +``` + +Use the `layout` attribute in the `page` node of a page configuration file to define a layout type for the page. The following example shows how to use the `3 columns` page layout type for the [Wish List Sharing]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Wishlist/view/frontend/layout/wishlist_index_share.xml#L8) page: + +Override the default `wishlist_index_share.xml` in any one of the following paths and add the `layout="3columns"` in the `page` node. + +* Override the layout in a custom `theme` (_in the case where a custom-built theme is applied on the storefront_): `/Magento_Wishlist/layout/wishlist_index_share.xml` +* Override the layout in custom `module` (_in case the when building third-party extensions and you need to make changes to the existing layout_): `/view/frontend/layout/wishlist_index_share.xml` + +```xml + + + + + + + + + + +``` + +![Wish List Sharing. 3 columns layout page type]({{ page.baseurl }}/frontend-dev-guide/images/wish-list-sharing.png) + +{:.bs-callout-info} +By default, Magento provides 5 page layout types for the frontend (`empty`, `1column`, `2columns-left`, `2columns-right`, and `3columns`) and 3 page layout types for the backend (`admin-empty`, `admin-1column`, and `admin-2columns-left`). + +## Page configuration {#layout-types-conf} + +The page configuration adds content to the wireframe defined in a page layout file. A page configuration also contains page meta-information, and contents of the `` section. + +### Page configuration file conventional location {#layout-type-conf-loc} + +Conventionally page configuration files must be located as follows: + +* Module page configurations: `/view/frontend/layout` +* Theme page configurations: `/_/layout` + +### Page configuration structure and allowed layout instructions + +The following table describes the instructions specific for page configuration files. For the descriptions of common layout instructions see the [Layout instructions]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html) article. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ElementAttributesParent ofDescription
    + <page></page> + +
      +
    • + layout = {layout} +
    • +
    • + xsi:noNamespaceSchemaLocation ="{path_to_schema}" +
    • +
    +
    +
      +
    • <html>
    • +
    • <head>
    • +
    • <body>
    • +
    • <update>
    • +
    +
    Mandatory root element.
    <html></html> +

    none

    +
    +
      +
    • <attribute>
    • +
    +
    <head></head>none +
      +
    • <title>
    • +
    • <meta>
    • +
    • <link>
    • +
    • <css>
    • +
    • <font>
    • +
    • <script>
    • +
    • <remove>
    • +
    • <attribute>
    • +
    +
    <body></body>none +
      +
    • <block>
    • +
    • <container>
    • +
    • <move>
    • +
    • <attribute>
    • +
    • <referenceBlock>
    • +
    • <referenceContainer>
    • +
    • <action>
    • +
    +
    <attribute> +
      +
    • name = {arbitrary_name} +
    • +
    • value = {arbitrary_value} +
    • +
    +
    +

    Specified for <html>, rendered like following:

    +

    <html name="value'>

    +
    +

    <title>

    +
    nonenonePage title
    +

    <meta>

    +
    +
      +
    • + content +
    • +
    • + charset +
    • +
    • + http-equiv +
    • +
    • + name +
    • +
    • + scheme +
    • +
    +
    + none +
    +

    <link>

    +
    +
      +
    • + defer +
    • +
    • + ie_condition +
    • +
    • + charset +
    • +
    • + hreflang +
    • +
    • + media +
    • +
    • + rel +
    • +
    • + rev +
    • +
    • + sizes +
    • +
    • + src +
    • +
    • + src_type +
    • +
    • + target +
    • +
    • + type +
    • +
    +
    + none +  
    + <css> + +
      +
    • + defer +
    • +
    • + ie_condition +
    • +
    • + charset +
    • +
    • + hreflang +
    • +
    • + media +
    • +
    • + rel +
    • +
    • + rev +
    • +
    • + sizes +
    • +
    • + src +
    • +
    • + src_type +
    • +
    • + target +
    • +
    • + type +
    • +
    +
    + none +
    +

    <script>

    +
    +
      +
    • + defer +
    • +
    • + ie_condition +
    • +
    • + async +
    • +
    • + charset +
    • +
    • + src +
    • +
    • + src_type +
    • +
    • + type +
    • +
    +
    + none +
    + +## Generic layout {#layout-types-gen} + +Generic layouts define the contents and detailed structure inside the `` section of the HTML page markup. + +### Generic layout file conventional location {#layout-type-gen-loc} + +Conventionally generic layout files must be located as follows: + +* Module generic layouts: `/view/frontend/layout` +* Theme generic layouts: `/_/layout` + +### Generic layout structure and allowed layout instructions + +The following table describes the instructions specific for generic layout files. For the descriptions of common layout instructions see the [Layout instructions]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-instructions.html) article. + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ElementAttributesParent ofDescription
    + <layout></layout> + +
      +
    • + + xsi:noNamespaceSchemaLocation="{path_to_schema}" + +
    • +
    +
    +
      +
    • <container>
    • +
    • <update>
    • +
    +
    Mandatory root element.
    + <update> + +
      +
    • + handle="{name_of_handle_to_include}" +
    • +
    +
    +none +
    <container> + + +
      +
    • <block>
    • +
    • <container>
    • +
    • <referenceBlock>
    • +
    • <referenceContainer>
    • +
    +
    Mandatory element
    + +Sample generic layout: + +```xml + + + + + + + +``` diff --git a/src/guides/v2.3/frontend-dev-guide/layouts/product-layouts.md b/src/guides/v2.3/frontend-dev-guide/layouts/product-layouts.md new file mode 100644 index 00000000000..698dc24c57f --- /dev/null +++ b/src/guides/v2.3/frontend-dev-guide/layouts/product-layouts.md @@ -0,0 +1,72 @@ +--- +group: frontend-developer-guide +title: Product layouts +functional_areas: + - Frontend +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +This topic provides information about product layouts files. Magento allows you to customize view pages for all product types in the common layout files. It is also possible to perform the customization for a particular product type or even for a concrete product page by Product Entity ID or SKU. + +## Product view page + +Layout file | Description +--- | --- +`catalog_product_view.xml` | Common layout. Affects all product types +`catalog_product_view_type_bundle.xml` | Layout from this file is applied to `bundle` product only +`catalog_product_view_type_configurable.xml` | Layout from this file is applied to `configurable` product only +`catalog_product_view_type_downloadable.xml` | Layout from this file is applied to `downloadable` product only +`catalog_product_view_type_grouped.xml` | Layout from this file is applied to `grouped` product only +`catalog_product_view_type_simple.xml` | Layout from this file is applied to `simple` product only +`catalog_product_view_type_virtual.xml` | Layout from this file is applied to `virtual` product only +`catalog_product_view_id_{id}.xml` | Layout from this file is applied to the specific product by `Entity ID` value. E.g. `catalog_product_view_id_45.xml` +`catalog_product_view_sku_{sku}.xml` | Layout from this file is applied to the specific product by `SKU` value. E.g. `catalog_product_view_sku_24-WG080.xml` + +## Customize product view pages + +Use containers on the product page to structure content in the layout. You can reference the container and add blocks to it. + +Containers assign content structure to a page using container tags within a layout XML file. A container has no additional content except the content of included elements. Examples of containers include: + +* `product.info.main` +* `product.info.price` +* `product.info.stock.sku` +* `product.info.form.content` +* `product.info.extrahint` +* `product.info.social` +* `product.info.media` + +### Example + +```xml + +``` + +## Checkout cart configure page + +Layout file | Description +--- | --- +`checkout_cart_configure.xml` | Common layout. Affects all product types +`checkout_cart_configure_type_bundle.xml` | Layout from this file is applied to `bundle` product only +`checkout_cart_configure_type_configurable.xml` | Layout from this file is applied to `configurable` product only +`checkout_cart_configure_type_downloadable.xml` | Layout from this file is applied to `downloadable` product only +`checkout_cart_configure_type_simple.xml` | Layout from this file is applied to `simple` product only +`checkout_cart_configure_id_{id}.xml` | Layout from this file is applied to the specific product by `Entity ID` value. E.g. `checkout_cart_configure_id_45.xml` +`checkout_cart_configure_sku_{sku}.xml` | Layout from this file is applied to the specific product by `SKU` value. E.g. `checkout_cart_configure_sku_24-WG080.xml` +`checkout_cart_item_renderers.xml` | Layout from this file is applied to renderer's cart page items + +## Wishlist item configure page + +Layout file | Description +--- | --- +`wishlist_index_configure.xml` | Common layout. Affects all product types +`wishlist_index_configure_type_bundle.xml` | Layout from this file is applied to `bundle` product only +`wishlist_index_configure_type_configurable.xml` | Layout from this file is applied to `configurable` product only +`wishlist_index_configure_type_downloadable.xml` | Layout from this file is applied to `downloadable` product only +`wishlist_index_configure_type_grouped.xml` | Layout from this file is applied to `grouped` product only +`wishlist_index_configure_type_simple.xml` | Layout from this file is applied to `simple` product only +`wishlist_index_configure_id_{id}.xml` | Layout from this file is applied to the specific product by `Entity ID` value. E.g. `wishlist_index_configure_id_45.xml` +`wishlist_index_configure_sku_{sku}.xml` | Layout from this file is applied to the specific product by `SKU` value. E.g. `wishlist_index_configure_sku_24-WG080.xml` + +For setting a custom layout on specific category, product, and CMS pages, see [Common layout customization tasks]({{ page.baseurl }}/frontend-dev-guide/layouts/xml-manage.html#create-cms-pageproductcategory-specific-selectable-layouts). diff --git a/src/guides/v2.3/frontend-dev-guide/layouts/xml-instructions.md b/src/guides/v2.3/frontend-dev-guide/layouts/xml-instructions.md new file mode 100644 index 00000000000..a45861e11ea --- /dev/null +++ b/src/guides/v2.3/frontend-dev-guide/layouts/xml-instructions.md @@ -0,0 +1,457 @@ +--- +group: frontend-developer-guide +title: Layout instructions +functional_areas: + - Frontend +--- + +## What's in this topic {#fedg_layout_xml-instruc_overview} + +There are two possible ways to customize page layout in Magento: + +- Changing [layout](https://glossary.magento.com/layout) files. +- Altering templates. + +To change the page wireframe, modify the [page layout] files; all other customizations are performed in the [page configuration] or [generic layout] files. + +## Manage layouts + +To make layout changes available on every page, modify the `default.xml` file. +For example, layout changes added to `app/code/Vendor/Module/view/frontend/layout/default.xml` are loaded on all pages. +To add layout changes to a specific page, use a layout file that corresponds to the page's path. +For example, changes to the `app/code/Vendor/Module/view/frontend/layout/catalog_product_view.xml` page are loaded on the product details page. + +Use these [layout instructions](https://glossary.magento.com/layout-instructions) to: + +- Move a page element to another parent element. +- Add content. +- Remove a page element. +- Arrange the element position. + +The basic set of instructions is the same for all types of layout files. This topic describes these basic instructions. For details about how they are used in a particular layout file type, please refer to the [Layout file types] topic. + +## Common layout instructions {#fedg_layout_xml-instruc_ex} + +Use the following layout instructions to customize your layout: + +- [``](#fedg_layout_xml-instruc_ex_block) +- [``](#fedg_layout_xml-instruc_ex_cont) +- [`before` and `after` attributes](#fedg_xml-instrux_before-after) +- [``](#fedg_layout_xml-instruc_ex_act) +- [`` and ``](#fedg_layout_xml-instruc_ex_ref) +- [``](#fedg_layout_xml-instruc_ex_mv) +- [``](#fedg_layout_xml-instruc_ex_rmv) +- [``](#fedg_layout_xml-instruc_ex_upd) +- [``](#argument) +- [` vs `](#block_vs_container) + +### block {#fedg_layout_xml-instruc_ex_block} + +Defines a block. + +**Details:** A block is a unit of page output that renders some distinctive content (anything visually tangible for the end-user), such as a piece of information or a user interface element. + +Blocks are a foundational building unit for layouts in Magento. They are the link between a PHP block class (which contains logic) and a template (which renders content). Blocks can have children and grandchildren (and so on). Information can be passed from layout XML files to blocks using the `` child node. + +Blocks employ templates to generate HTML. Examples of blocks include a [category](https://glossary.magento.com/category) list, a mini cart, product tags, and product listing. + +{:.bs-callout-info} +We recommend always adding a `name` to blocks. Otherwise, it is given a random name. + +| Attribute | Description | Values | Required? | +|:------- |:------ |:------ |:------ | +| `class` | Name of a class that implements rendering of a particular block. An object of this class is responsible for actual rendering of block output. | A fully-qualified class name, such as `Vendor\Module\Block\Class`. Defaults to `Magento\Framework\View\Element\Template`. | no | +| `display` | Prevents a block from displaying (the associated PHP classes are still loaded). | `true` or `false`. Defaults to `true`. | no | +| `name` | Name that can be used to address the block to which this attribute is assigned. The name must be unique per generated page. If not specified, an automatic name will be assigned in the format ANONYMOUS_n | 0-9, A-Z, a-z, underscore (_), period (.), dash (-). Should start with a letter. Case-sensitive. | no | +| `before` | Used to position the block before an element under the same parent. The element name or alias name is specified in the value. Use dash (-) to position the block before all other elements of its level of nesting. See [before and after attributes](#fedg_xml-instrux_before-after) for details. | Element name or dash (-) | no | +| `after` | Used to position the block after an element under the same parent. The element name or alias name is specified in the value. Use dash (-) to position the block after all other elements of its level of nesting. See [before and after attributes](#fedg_xml-instrux_before-after) for details. | Element name or dash (-) | no | +| `template` | A template that represents the functionality of the block to which this attribute is assigned. If the attribute is omitted, the block will not render any output unless the block class (or a parent class) has the `$_template` property defined correctly. | `Vendor_Module::path/to/template.phtml` (Scope is already in the `templates` directory of the module) | no | +| `as` | An alias name that serves as identifier in the scope of the parent element. | 0-9, A-Z, a-z, underscore (_), period (.), dash (-). Case-sensitive. | no | +| `cacheable` | Defines whether a block element is cacheable. This can be used for development purposes and to make needed elements of the page dynamic. | `true` or `false`. Defaults to `true`. | no | +| `ifconfig` | Makes the block's visibility dependent on a system configuration field. | XPath to the system configuration field. E.g. `contact/contact/enabled` | no | + +To pass parameters use the [``](#argument) instruction. + +Sample of usage in the product listing page layout: + +```xml + +``` + +### container {#fedg_layout_xml-instruc_ex_cont} + +A structure without content that holds other layout elements such as blocks and containers. + +**Details:** +A container renders child elements during view output generation. It can be empty or it can contain an arbitrary set of `` and `` elements. If the `` is empty, and there is no child `` available, it will not be displayed in the frontend source code. + +{:.bs-callout-info} +We recommend always adding a `name` to containers. Otherwise, it is given a random name. + +| Attribute | Description | Values | Required? | +|:------- |:------ |:------ |:------ | +| `name` | A name that can be used to address the container in which this attribute is assigned. The name must be unique per generated page. If not specified, it will be auto-generated. | A-Z, a-z, 0-9, underscore (_), period (.), dash (-). Should start with a letter. Case-sensitive. | No | +| `label` | Describes the purpose of the container. | Any | No | +| `before` | Used to position the container before an element under the same parent. The element name or alias name is specified in the value. Use dash (-) to position the block before all other elements of its level of nesting. See [before and after attributes](#fedg_xml-instrux_before-after) for details. | Element name or dash (`-`) | No | +| `after` | Used to position the container after an element under the same parent. The element name or alias name is specified in the value. Use dash (-) to position the block after all other elements of its level of nesting. See [before and after attributes](#fedg_xml-instrux_before-after) for details. | Element name or dash (-). | No | +| `as` | An alias name that serves as identifier in the scope of the parent element. | 0-9, A-Z, a-z, underscore (_), period (.), dash (-). Case-sensitive. | No | +| `output` | Defines whether to output the root element. If specified, the element will be added to output list. (If not specified, the parent element is responsible for rendering its children.) | Any value except the obsolete `toHtml`. Recommended value is `1`. | No | +| `htmlTag` | Output parameter. If specified, the output is wrapped into specified HTML tag. | Any of the following: `aside`, `dd`, `div`, `dl`, `fieldset`, `main`, `nav`, `header`, `footer`, `ol`, `p`, `section`, `table`, `tfoot`, `ul` | No, Yes - if `htmlClass` or `htmlId` is specified | +| `htmlId` | Output parameter. If specified, the value is added to the wrapper element. If there is no wrapper element, this attribute has no effect. | Any valid HTML 5 `id` value. | No | +| `htmlClass` | Output parameter. If specified, the value is added to the wrapper element. If there is no wrapper element, this attribute has no effect. | Any valid HTML 5 `class` value. | No | + +Sample of usage in layout: + +```xml + + + +``` + +This would add a new column to the page layout. + +#### Controlling children visibility + +The `output` attribute controls the visibility of the container's children elements. +Set this value to `1` to render children content or `0` to disable the output of the entire container. + +Use this feature to make temporary changes to a store, such as disabling a section of the page for a sales event and re-enabling it after the event ends. + +### block vs. container {#block_vs_container} + +- Blocks represents the end of the chain in rendering HTML for Magento. +- Containers contain blocks and can wrap them in an HTML tag. +- Containers do not render any output if there are no children assigned to them. + +### before and after attributes {#fedg_xml-instrux_before-after} + +To help you to position elements in a specific order suitable for design, SEO, usability, or other requirements, Magento software provides the `before` and `after` layout attributes. +These optional attributes can be used in layout XML files to control the order of elements in their common parent. + +The following tables give a detailed description of the results you can get using the `before` and `after` attributes. The first table uses a block a as positioned element. + +| Attribute | Value | Description | +|:------- |:------ |:------ | +| `before` | Dash (-) | The block displays before all other elements in its parent node. | +| `before` | [element name] | The block displays before the named element. | +| `before` | Empty value or [element name] is absent | Use the value of `after`. If that value is empty or absent as well, the element is considered as non-positioned. | +| `after` | Dash (-) | The block displays after all other elements in its parent node. | +| `after` | [element name] | The block displays after the named element. | +| `after` | Empty value or [element name] is absent | Use the value of `before`. If that value is empty or absent as well, the block is considered as non-positioned. | + +#### Examples {#examples} + +| Situation | Result | +|:------- |:------ | +| Both `before` and `after` attributes are present | `after` takes precedence. | +| Both `before` and `after` attributes are absent or empty | The element is considered as non-positioned. All other elements are positioned at their specified locations. The non-positioned element displays at a random position that doesn't violate requirements for the positioned elements. | +| Several elements have `before` or `after` set to dash (-) | All elements display at the top (or bottom, in case of the after attribute), but the ordering of group of these elements is undefined. | +| The `before` or `after` attribute's value refers to an element that is not located in the parent node of the element being defined. | The element displays at a random location that doesn't violate requirements for the correctly positioned elements. | + +Sample usage in a layout: + +```xml + + + + + +``` + +### action {#fedg_layout_xml-instruc_ex_act} + +{:.bs-callout-warning} +The `` instruction is deprecated. If the method implementation allows, use the [``](#argument) for [``](#fedg_layout_xml-instruc_ex_block) or [``](#fedg_layout_xml-instruc_ex_ref) to access the block public API. + +Calls public methods on the block API. + +**Details:** Used to set up the execution of a certain method of the block during block generation; the `` node must be located in the scope of the `` node. + +```xml + + + Text + + + true + + +``` + +`` child nodes are translated into block method arguments. Child nodes names are arbitrary. If there are two or more nodes with the same name under ``, they are passed as one array. + +| Attribute | Description | Values | Required? | +|:------- |:------ |:------ |:------ | +| `method` | The public method that is called during block generation. | The method name in the block | yes | + +To pass parameters, use the [``](#argument) instruction. + +### referenceBlock and referenceContainer {#fedg_layout_xml-instruc_ex_ref} + +Updates in `` and `` are applied to the corresponding `` or ``. + +For example, if you make a reference by ``, you are targeting the block ``. + +To pass parameters to a block use the [``](#argument) instruction. + +| Attribute | Description | Values | Required? | +|:------- |:------ |:------ |:------ | +| `remove` | Allows to remove or cancel the removal of the element. When a container is removed, its child elements are removed as well. | `true` or `false` | no | +| `display` | Allows you to disable rendering of specific block or container with all its children (both set directly and by reference). The PHP objects of the block or container and its children are still generated and available for manipulation. | `true` or `false` | no | + +- The `remove` attribute is optional and its default value is `false`. + + This implementation allows you to remove a block or container in your layout by setting the remove attribute value to `true`, or to cancel the removal of a block or container by setting the value to `false`. + + ```xml + + ``` + +- The `display` attribute is optional and its default value is true. + + You are always able to overwrite this value in your layout. + In situation when remove value is true, the display attribute is ignored. + + ```xml + + ``` + +### move {#fedg_layout_xml-instruc_ex_mv} + +Sets the declared block or container element as a child of another element in the specified order. + +```xml + +``` + +- `` is skipped if the element to be moved is not defined. +- If the `as` attribute is not defined, the current value of the element alias is used. If that is not possible, the value of the `name` attribute is used instead. +- During layout generation, the `` instruction is processed before the removal (set using the `remove` attribute). This means if any elements are moved to the element scheduled for removal, they will be removed as well. + +| Attribute | Description | Values | Required? | +|:------- |:------ |:------ |:------ | +| `element` | Name of the element to move. | Element name | yes | +| `destination` | Name of the target parent element. | Element name | yes | +| `as` | Alias name for the element in the new location. | 0-9, A-Z, a-z, underscore (_), period (.), dash (-). Case-sensitive. | no | +| `after` or `before` | Specifies the element's position relative to siblings. Use dash (-) to position the block before or after all other siblings of its level of nesting. If the attribute is omitted, the element is placed after all siblings. | Element name | no | + +Sample of usage in the page layout: + +```xml + +``` + +### remove {#fedg_layout_xml-instruc_ex_rmv} + +`` is used only to remove the static resources linked in a page `` section. +For removing blocks or containers, use the `remove` attribute for [`` and ``](#fedg_layout_xml-instruc_ex_ref). + +```xml + + + + + + + + + + + + + +``` + +### update {#fedg_layout_xml-instruc_ex_upd} + +Includes a certain layout file. + +```xml + +``` + +The specified [handle] is "included" and executed recursively. + +Sample of usage in the page layout: + +```xml + +``` + +### argument {#argument} + + {:.bs-callout-info} +Magento 2.3.2 added the `shared` attribute. Now, instances of the view models are shared by default. If a view model is required to be a new instance each time, you must add the attribute `shared="false"` on the argument node in the layout xml file. + +Used to pass an argument. Must be always enclosed in [``](#arguments). + +| Attribute | Description | Values | Required? | +|:------- |:------ |:------ |:------ | +| `name` | Argument name. | unique | yes | +| `shared` | If false, creates a new instance of the block. | `false` | no | +| `translate` | Specify whether the string is translatable or not | `true` or `false` | no | +| `xsi:type` | Argument type. | `string`, `boolean`, `object`, `number`, `null`, `array`, `options`, `url`, `helper` | yes | + +To pass multiple arguments use the following construction: + +```xml + + Custom string + true + ... + +``` + +Arguments values set in a layout file can be accessed in [templates] using the `getData('{ArgumentName}')` and `hasData('{ArgumentName}')` methods. The latter returns a boolean defining whether there's any value set. +`{ArgumentName}` is obtained from the `name` attribute the following way: for getting the value of `` the method name is `getData('some_string')`. + +**Example:** + +Setting a value of `css_class` in the `[app/code/Magento/Theme/view/frontend/layout/default.xml]` layout file: + +```xml + + header links + +``` + +Using the value of `css_class` in `[app/code/Magento/Theme/view/frontend/templates/html/title.phtml]`: + +```php +$cssClass = $this->hasCssClass() ? ' ' . $this->getCssClass() : ''; +``` + +#### Argument types examples + +As was described above the argument attribute can be added with different types. +There are examples of all argument types. + +- The *string* type: + +```xml +Some String +``` + +- The *boolean* type: + +```xml +true +``` + +- The *object* type: + +```xml +Vendor\CustomModule\ViewModel\Class +``` + +The `Vendor\CustomModule\ViewModel\Class` class should implement the `\Magento\Framework\View\Element\Block\ArgumentInterface` interface. + +- The *number* type: + +```xml +100 +``` + +- The *null* type: + +```xml + +``` + +- The *array* type: + +```xml + + First Item + Second Item + ... + +``` + +- The *options* type: + +```xml +Vendor\CustomModule\Source\Options\Class +``` + +The `Vendor\CustomModule\Source\Options\Class` class should implement the `\Magento\Framework\Data\OptionSourceInterface` interface. + +- The *url* type: + +```xml + + param1value + param2value + ... + +``` + +The *url* may have parameters, but they are optional. + +- The *helper* type: + +```xml + + firstValue + secondValue + ... + +``` + +The *helper* can use only public methods. In this example the `someMethod()` method should be public. +The argument with *helper* type can contain `param` items which can be passed as a helper method parameters. + +#### Obtain arguments examples in template + +These argument examples can be taken in the template by *getData* method. Another way to take these arguments is using the magic method *get* followed by the name of argument in CamelCase format. Here is an example to retrieve the arguments from above example: + +```php +getData('some_string'); //or $block->getSomeString() + +/** @var bool $isActive */ +$isActive = $block->getData('is_active'); //or $block->getIsActive() + +/** @var Vendor\CustomModule\ViewModel\Class|\Magento\Framework\View\Element\Block\ArgumentInterface $viewModel */ +$viewModel = $block->getData('view_model'); //or $block->getViewModel() + +/** @var string|int|float $someNumber */ +$someNumber = $block->getData('some_number'); //or $block->getSomeNumber() + +/** @var null $nullValue */ +$nullValue = $block->getData('null_value'); //or $block->getNullValue() + +/** @var array $customArray */ +$customArray = $block->getData('custom_array'); //or $block->getCustomArray() + +/** @var array $options */ +$options = $block->getData('options'); //or $block->getoptions() + +/** @var string $shoppingCartUrl */ +$shoppingCartUrl = $block->getData('shopping_cart_url'); //or $block->getShoppingCartUrl() + +/** @var mixed $helperMethodResult */ +$helperMethodResult = $block->getData('helper_method_result'); // or $block->getHelperMethodResult() +``` + +### arguments {#arguments} + +`` is a required container for ``. It does not have its own attributes. + +```xml + + header links + +``` + +[page layout]: {{page.baseurl}}/frontend-dev-guide/layouts/layout-types.html#layout-types-page +[page configuration]: {{page.baseurl}}/frontend-dev-guide/layouts/layout-types.html#layout-types-conf +[generic layout]: {{page.baseurl}}/frontend-dev-guide/layouts/layout-types.html#layout-types-gen +[handle]: {{page.baseurl}}/frontend-dev-guide/layouts/layout-overview.html#layout-over-terms +[templates]: {{page.baseurl}}/frontend-dev-guide/templates/template-overview.html +[app/code/Magento/Theme/view/frontend/layout/default.xml]: {{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Theme/view/frontend/layout/default.xml +[app/code/Magento/Theme/view/frontend/templates/html/title.phtml]: {{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Theme/view/frontend/templates/html/title.phtml +[Layout file types]: {{page.baseurl}}/frontend-dev-guide/layouts/layout-types.html diff --git a/src/guides/v2.3/frontend-dev-guide/layouts/xml-manage.md b/src/guides/v2.3/frontend-dev-guide/layouts/xml-manage.md new file mode 100644 index 00000000000..cdf27388890 --- /dev/null +++ b/src/guides/v2.3/frontend-dev-guide/layouts/xml-manage.md @@ -0,0 +1,765 @@ +--- +group: frontend-developer-guide +title: Common layout customization tasks +functional_areas: + - Frontend +--- + +## In this topic + +This article describes the following typical [layout](https://glossary.magento.com/layout) customization tasks: + +- [Set the page layout](#layout_markup_columns) +- [Include static resources (JavaScript, CSS, fonts) in \](#layout_markup_css) +- [Remove static resources (JavaScript, CSS, fonts) in \](#layout_markup_css_remove) +- [Add meta tags to the head block](#layout_markup_meta) +- [Create a container](#create_cont) +- [Reference a container](#ref_container) +- [Reference a CMS block](#ref_cms_block) +- [Making the block visibility dynamic](#ref_config_block) +- [Create a block](#xml-manage-block) +- [Set body attributes](#layout_body_attributes) +- [Set the template used by a block](#set_template) +- [Modify block arguments](#layout_markup_modify-block) +- [Reference a block](#xml-manage-ref-block) +- [Use block object methods to set block properties](#layout_markup_block-properties) +- [Rearrange elements](#layout_markup_rearrange) +- [Add functionality to existing elements](#layout_markup_add_to_elements) +- [Modify functionality with plugins (interceptors)](#layout_markup_modify_with_plugins) + +{:.bs-callout-info} +To ensure stability and secure your customizations from being deleted during upgrade, do not change out-of-the-box Magento [module](https://glossary.magento.com/module) and [theme](https://glossary.magento.com/theme) layouts. To customize your layout, create extending and overriding layout files in your custom theme. + +## Set the page layout {#layout_markup_columns} + +The type of page layout to be used for a certain page is defined in the page configuration file, in the `layout` attribute of the root `` node. + +Example: +Change the layout of Advanced Search page from default "1-column" to "2-column with left bar". To do this, extend `catalogsearch_advanced_index.xml` in your theme by adding the following layout: + +```xml + +... + +``` + +## Include static resources (JavaScript, CSS, fonts) {#layout_markup_css} + +JavaScript, CSS, and other static assets are added in the `` section of a [page configuration] file. The default look of a Magento store page `` is defined by `app/code/Magento/Theme/view/frontend/layout/default_head_blocks.xml`. The recommended way to add CSS and JavaScript is to extend this file in your custom theme, and add the assets there. +The following file is a sample of a file you must add: + +```xml + + + + + + + + +``` + +## Form validation rules + +All available Magento validation rules may be found in [validation/rules.js]({{ site.mage2bloburl }}/{{page.guide_version}}/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js). + +Additionally, you may also use any available [jQuery validation rules](https://jqueryvalidation.org/documentation/#link-list-of-built-in-validation-methods). + +## Defining validation rules + +There are couple of ways to define validation rules for a form field. + +### As a `data-validate` attribute + +```html + +``` + +### As a `data-validate` attribute with arguments + +```html + +``` + +### As an attribute + +```html + +``` + +### As an attribute with arguments + +```html + +``` + +### As a class name + +```html + +``` + +### As a class name with arguments + +```html + +``` + +### Using `data-mage-init` + +```html +
    + ... +
    +``` +### Using `data-mage-init` with arguments + +```html +
    + ... +
    +``` + +## Examples + +### Adding the validation for a form + +Here are examples of all available ways of validating the form fields. + +```html +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +``` + +### Result + +As a result, the form gets validated before sending data to the server for processing. + +![Validated Form Example]({{ site.baseurl }}/common/images/form-validation-result.png) diff --git a/src/guides/v2.3/frontend-dev-guide/validations/custom-validation.md b/src/guides/v2.3/frontend-dev-guide/validations/custom-validation.md new file mode 100644 index 00000000000..5dc0576e377 --- /dev/null +++ b/src/guides/v2.3/frontend-dev-guide/validations/custom-validation.md @@ -0,0 +1,99 @@ +--- +group: frontend-developer-guide +title: Custom validation rules +contributor_name: Adarsh Manickam +contributor_link: https://github.com/drpayyne +--- + +Custom validation rules can be added by creating a Javascript mixin for the `mage/validation` module and calling the `$.validator.addMethod` function with the custom validation rule parameters as described below: + +```javascript +$.validator.addMethod( + 'rule-name', + function(value, element) { + // Return true or false after validation rule check + }, + $.mage.__('Error message to display if validation fails') +) +``` + +This code snippet adds a simple new validation rule to the mixin to validate if an input field has only five words. + +`Vendor/Module/view/frontend/requirejs-config.js` + +```javascript +var config = { + config: { + mixins: { + 'mage/validation': { + 'Vendor_Module/js/validation-mixin': true + } + } + } +} +``` + +`Vendor/Module/view/frontend/web/js/validation-mixin.js` + +```javascript +define(['jquery'], function($) { + 'use strict'; + + return function(targetWidget) { + $.validator.addMethod( + 'validate-five-words', + function(value, element) { + return value.split(' ').length == 5; + }, + $.mage.__('Please enter exactly five words') + ) + return targetWidget; + } +}); +``` + +## Modify an existing validation message + +It is possible to adjust the existing error message for form fields. +This is implemented in the core codebase in scope of the [`Magento_CatalogSearch` module]({{ site.mage2bloburl }}/{{page.guide_version}}/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml). + +```html + +``` + +The `messages` object is the one that does the job - they key is the input name and the value is a list of validation rules that should be modified for the specified input field. +Here the rule name is the key and the validation message is the value. + +```js +$('#form-to-validate').mage('validation', { + messages: { + 'input-name': { + 'validation-rule-1': 'Validation message 1', + 'validation-rule-2': 'Validation message 2', + }, + } +}); +``` + +This comes in handy when the error message needs to be specific but the rule does not change. diff --git a/src/guides/v2.3/frontend-dev-guide/validations/form-validation.md b/src/guides/v2.3/frontend-dev-guide/validations/form-validation.md new file mode 100644 index 00000000000..bc592d4c65e --- /dev/null +++ b/src/guides/v2.3/frontend-dev-guide/validations/form-validation.md @@ -0,0 +1,30 @@ +--- +group: frontend-developer-guide +title: Form validation +contributor_name: Adarsh Manickam +contributor_link: https://github.com/drpayyne +functional_areas: + - Frontend +--- + +The Magento application provides various ways to validate your form inputs. This implementation is based, and extends, [jQuery Validation](https://jqueryvalidation.org/documentation). + +## Validation Module Structure + +There are three main validation modules present in Magento: `jquery/validate`, `mage/validation`, and `mage/validation/validation`. + +### `jquery/validate` + +This is an alias for [`lib/web/jquery/jquery.validate`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/web/jquery/jquery.validate.js). This is the base validation JavaScript file provided by jQuery that Magento extends. + +### `mage/validation` + +This module is present at [`lib/web/mage/validation.js`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/web/mage/validation.js). This module includes `jquery/validate` and adds various functions, such as `$.validator.addMethod`, which can be used by mixins to add custom validation rules, a base set of rules to validate, the `mage.validation` widget, and more. + +### `mage/validation/validation` + +This module is present at [`lib/web/mage/validation/validation.js`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/web/mage/validation/validation.js). This is considered the entry point for the form validator in Magento and is aliased as `validation` at [`Magento_Theme/view/frontend/requirejs-config.js`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Theme/view/frontend/requirejs-config.js#L29). This includes `mage/validation` (which in turn includes `jquery/validate`), and adds a few more rules to the validator. + +## See also + +* [Validate a custom form]({{ page.baseurl }}/frontend-dev-guide/validations/custom-form-validation.html) diff --git a/src/guides/v2.3/frontend-dev-guide/validations/rule-list.md b/src/guides/v2.3/frontend-dev-guide/validations/rule-list.md new file mode 100644 index 00000000000..c49ec9c1036 --- /dev/null +++ b/src/guides/v2.3/frontend-dev-guide/validations/rule-list.md @@ -0,0 +1,1676 @@ +--- +group: frontend-developer-guide +title: Validation Rule List +contributor_name: Goivvy LLC +contributor_link: https://www.goivvy.com/magento-optimization-service +--- + +This is a list of available form validation rules, ordered alphabetically. +Each rule contains a short description and a usage example. + +### alphanumeric + +Check if the value contains only letters, numbers, spaces or underscores. + +#### Example + +```html +
    +... + +... +
    +``` + +### credit-card-types + +A valid credit card number of a certain type(s), that can be specified as parameters. + +#### Example + +```html +
    +... + +... +
    +``` + +Possible values are: amex, mastercard, visa, dinersclub, enroute, discover, jcb, unknown, all. + +### dateITA + +Date in Italy, **\d{1,2}\/\d{1,2}\/\d{4}** format, i.e. **4/4/24** or **24/12/21**. + +#### Example + +```html +
    +... + +... +
    +``` + +* `44/44/40` - false +* `12-12-2021` - false +* `1/1/2022` - true +* `12/10/2022` - true + +There is a sanity check, so dates such as `99/12/2021` will be false. + +### dateNL + +Date in Netherlands, **\d\d?[\.\/-]\d\d?[\.\/-]\d\d\d?\d?** format. + +### Example + +```html +
    +... + +... +
    +``` + +There is no sanity check so dates such as `33-12-12` will be true. + +### datetime-validation + +Checks that the field is not empty. + +### Example + +```html +
    +... + +... +
    +``` + +### email2 + +Checks for a valid email address. + +#### Example + +```html +
    +... + +... +
    +``` + +### greater-than-equals-to + +Checks for a value of one field being greater than or equal to a value of another field + +#### Example + +```html +
    +... + + +... +
    +``` + +It does not check for both values to be numeric, so if `field-3` = 5 and `field-5` = 'a', it will silently accept it + +### integer + +Checks for a field value to be an integer, positive or negative. + +#### Example + +```html +
    +... + +... +
    +``` + +### ipv4 + +Checks for a valid IPv4 address. + +#### Example + +```html +
    +... + +... +
    +``` + +### ipv6 + +Checks for a valid IPv6 address. + +#### Example + +```html +
    +... + +... +
    +``` + +### less-than-equals-to + +Checks for a value of one field being less than or equal to a value of another field. + +#### Example + +```html +
    +... + + +... +
    +``` + +It does not check for both values to be numeric, so if `field-3` = 10 and `field-5` = '3a', it will silently accept it. + +### letters-only + +Checks for Latin A-Z,a-z letters only. + +#### Example + +```html +
    +... + +... +
    +``` + +### letters-with-basic-punc + +Checks for Latin letters and punctuation only, the regex being `a-z\-.,()'\"\s`. + +```html +
    +... + +... +
    +``` + +### max-words + +Checks that there are no more than a predefined number of words. Maximum number of words should be set as a parameter. + +#### Example + +```html +
    +... + +... +
    +``` + +Here, it accepts no more than 4 words. + +### min-words + +Checks that there are not less than a predefined number of words. Minimum number of words should be set as a parameter. + +#### Example + +```html +
    +... + +... +
    +``` + +Here, it accepts at least 4 words. + +### mobileUK + +Checks for a valid UK mobile number. + +#### Example + +```html +
    +... + +... +
    +``` + +* `+447911123456` - true +* `44791112` - false + +### no-marginal-whitespace + +Does not allow whitespaces at the start, or at the end, of an input text. + +#### Example + +```html +
    +... + +... +
    +``` + +### no-whitespace + +Does not allow whitespaces anywhere in an input text. + +#### Example + +```html +
    +... + +... +
    +``` + +### not-negative-amount + +Checks for a non-negative number. + +#### Example + +```html +
    +... + +... +
    +``` + +There is a sanity check, so **0a** will result in a warning. + +* `0` - pass +* `2.4` - pass +* `0a` - fail +* `+2` - fail + +### password-not-equal-to-user-name + +Checks that a password is not the same as a predefined string. + +#### Example + +```html +
    +... + +... +
    +``` + +In the example above, if you enter `username@domain.com` you will get a warning. + +### pattern + +Checks an input against a predefined regex pattern. + +#### Example + +```html +
    +... + +... +
    +``` + +In the example above anything except lowercase letters will trigger a warning. + +### phoneUK + +Checks for a valid UK phone number. + +#### Example + +```html +
    +... + +... +
    +``` + +### phoneUS + +Checks for a valid US phone number. + +#### Example + +```html +
    +... + +... +
    +``` + +### range-words + +Checks for a predefined number of words. + +#### Example + +```html +
    +... + +... +
    +``` + +In the example above, only a 2, 3 or 4 word input string will not trigger a warning: + +* `not used` - pass +* `not` - fail +* `not used before` - pass + +### required-dropdown-attribute-entry + +Checks that all disabled input fields (with a `required-option` class) within a table tag are empty. + +#### Example + +```html +
    + + + + + + +
    +
    +
    + +
    +
    +
    +``` + +In the example above it will trigger a warning as `field-1` is not empty. + +### required-entry + +Checks that a field is not empty. + +#### Example + +```html +
    +... + +... +
    +``` + +### required-file + +Checks for a file field to be populated. + +#### Example + +```html +
    +... + +... +
    +``` + +### required-if-all-sku-empty-and-file-not-loaded + +Makes the field required if specified fields and a specified file input are empty. + +#### Example + +```html +
    +... + + + +... +
    +``` + +In the example above, a warning is only displayed if all fields are empty. + +### required-if-not-specified + +Makes a field required if the dependent field is filled. + +#### Example + +```html +
    +... + + +... +
    +``` + +If `field-5` is not empty, then `field-3` is not required. + +### required-if-specified + +Makes a field required if the dependent field is filled. + +#### Example + +```html +
    +... + + +... +
    +``` + +If `field-5` is not empty, then `field-3` is required. + +### required-number + +Requires a number to be entered. + +```html +
    +... + +... +
    +``` + +Warning : This tag has bugs and will accept any non-empty input. + +### required-number-if-specified + +Makes a field number required if the dependent field is filled. + +#### Example + +```html +
    +... + + +... +
    +``` + +If `field-5` is not empty, then `field-3` is required, although any input will work, not just numbers. + +### required-text-swatch-entry + +Checks that all disabled input fields (with a `required-option` class) within a table tag to be empty. + +#### Example + +```html +
    + + + + + + +
    +
    +
    + +
    +
    +
    +``` + +In the example above, it will trigger a warning as `field-1` is not empty. + +### required-visual-swatch-entry + +Checks that all disabled input fields (with a `required-option` class) within a table tag are empty. + +#### Example + +```html +
    + + + + + + +
    +
    +
    + +
    +
    +
    +``` + +In the example above it will trigger a warning as `field-1` is not empty. + +### stripped-min-length + +Checks that there are at least a predefined number of characters in the input. + +#### Example + +```html +
    +... + +... +
    +``` + +In the example above, any input less than 4 characters will trigger a warning. + +### time + +Checks for a valid time between 00:00 and 23:59:59. + +#### Example + +```html +
    +... + +... +
    +``` + +### time12h + +Checks for a valid time between 00:00 am and 12:00 pm. `12:01 pm` and `11:59 pm` values are also valid. + +#### Example + +```html +
    +... + +... +
    +``` + +Warning : The implementation has bugs, for example a valid time `00:31 am` triggers a warning. + +### url2 + +Checks for a valid URL link. + +#### Example + +```html +
    +... + +... +
    +``` + +* `http://www.m2.com` - true +* `magento.com` - false + +### validate-admin-password + +Checks for a valid admin password. It must be 7 or more characters long and it has to have letters and numbers only. + +#### Example + +```html +
    +... + +... +
    +``` + +* `admindsdsdsd` - false +* `minsdsdss8` - true + +### validate-ajax-error + +Checks for an AJAX error. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-alpha + +Checks for letters (a-z or A-Z) only. + +#### Example + +```html +
    +... + +... +
    +``` + +* `jkjkjk` - true +* `dfdfdf1` - false + +### validate-alphanum + +Checks for letters (a-z or A-Z) or numbers (0-9) only. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-alphanum-with-spaces + +Checks for letters (a-z or A-Z) or numbers (0-9) or spaces only. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-cc-cvn + +Checks for a valid credit card identification number. + +#### Example + +```html +
    +... + + +... +
    +``` + +### validate-cc-exp + +Checks for a valid credit card expiration month. + +#### Example + +```html +
    +... + + +... +
    +``` + +It interprets input as a month number with year set in a predefined field. Sanity check is limited. + +* `40` - true +* `G` - false + +### validate-cc-number + +Checks for a valid credit card number based on mod 10. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-cc-type + +Checks for a credit card number to match a predefined credit card type. + +#### Example + +```html +
    +... + + +... +
    +``` + +Field `field-5` holds a credit card type, possible values are: + +* `SO` - Solo +* `SM` - Switch/Maestro +* `VI` - Visa +* `MC` - MasterCard +* `AE` - American Express +* `DI` - Discover +* `JCB` - JCB (Japan Credit Bureau) +* `DN` - Diners +* `UN` - UN +* `MI` - Maestro International +* `MD` - Maestro Domestic + +### validate-cc-type-select + +Checks for a credit card type to match a predefined credit card number. + +#### Example + +```html +
    +... + + +... +
    +``` + +### validate-cc-ukss + +Checks that the Switch/Solo/Maestro issue number and start date are filled. + +#### Example + +```html +
    +... + +... +
    +``` + +It does not do any sanity check except that a field is not empty. + +### validate-clean-url + +Checks for a valid URL. Protocol type is not necessary. + +#### Example + +```html +
    +... + +... +
    +``` + +* `fsdsd` - false +* `https://www.domain.com` - true +* `http://domain.com` - false +* `www.domain.com` - true +* `domain.com` - false + +### validate-code + +Checks for an input that has only letters (a-z or A-Z), numbers (0-9) or underscore (\_), and the first character should be a letter. + +#### Example + +```html +
    +... + +... +
    +``` + +* `1ddf` - false +* `Ad` - true + +### validate-cpassword + +Checks for a confirmation password to be the same as the password. + +#### Example + +```html +
    +... + + +... +
    +``` + +It is important to have `password` and `confirmation` IDs for the fields above. + +### validate-css-length + +Checks for a valid CSS length (Ex: 100px, 77pt, 20em, .5ex or 50%). + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-currency-dollar + +Checks for a valid US dollar amount, for example $100. + +#### Example + +```html +
    +... + +... +
    +``` + +* `$100` - true +* `200` - true +* `$ 100` - false + +### validate-customer-password + +Checks for a password to be greater or equal to a predefined number of characters and predefined number of character classes. Classes of characters: Lowercase, Uppercase, Digits, Special Characters. + +#### Example + +```html +
    +... + +... +
    +``` + +In the example above the password must be 10 or more characters long and have two or more different character classes: + +* `dfdfdfdfdfdfdfdfdfdfdfdf` - false +* `dfdfdfdfdfdfdfdfdfdfdfdf1` - true +* `d1$` - false + +### validate-data + +Checks for an input to have only letters (a-z or A-Z), numbers (0-9) or underscore (\_), and the first character should be a letter. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-date + +Checks for a valid date against a predefined format. + +#### Example + +```html +
    +... + +... +
    +``` + +In the example above: + +* `09 09` - true +* `dfdf` - false +* `98-98` - false + +### validate-date-au + +Checks for a valid date in the format: dd/mm/yyyy. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-date-range + +Checks for `From`-`To` date range. + +#### Example + +```html +
    +... + + +... +
    +``` + +### validate-digits + +Checks for a digits only input. + +#### Example + +```html +
    +... + +... +
    +``` + +* `sdsd` - false +* `34` - true + +### validate-digits-range + +Checks for a digits only input within a specified range. There are two ways to specify a range. You can specify a negative number as a range limit. + +#### Example + +```html +
    +... + + +... +
    +``` + +In the example above, the first range is from `-10` to `-9` and the second range is from `10` to `34`. + +### validate-email + +Checks for a valid email address. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-emails + +Checks for a valid email(s) separated (if several) by a comma, newline or a space. + +#### Example + +```html +
    +... + +... +
    +``` + +* `test@test.com` - true +* `test@test.com,test@test2.com` - true +* `test@test.com - test@test2.com` - false + +### validate-emailSender + +Checks for a valid email address although no sanity check is performed, i.e. any input is valid. Regex is `^[\S ]+$`. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-fax + +Checks for a valid fax number. + +#### Example + +```html +
    +... + +... +
    +``` + +* `044-434-3434` - true +* `111 222-2323` - true +* `111-12-2323` - false + +### validate-forbidden-extensions + +Checks that an input (comma separated file extensions) does not have an extension from a predefined list. + +### Example + +```html +
    +... + +... +
    +``` + +* `ddff` - true +* `jpg,png` - false +* `ddf` - false + +### validate-greater-than-zero + +Checks for a number greater than zero. There is a sanity check so `dfdf` input will trigger a warning. + +#### Example + +```html +
    +... + +... +
    +``` + +* `r4` - false +* `3.4` - true +* `+1.3` - true +* `0` - false + +### validate-identifier + +Checks for a valid URL key. + +#### Example + +```html +
    +... + +... +
    +``` + +* `dfdfdf` - true +* `hepee.html` - true +* `fdf$%.html` - false + +### validate-item-quantity + +Checks for a quantity number to be within `minAllowed` and `maxAllowed` and to be in `qtyIncremenets`. + +#### Example + +```html +
    +... + +... +
    +``` + +* `9` - false +* `28` - true +* `29` - false +* `300` - false + +### validate-length + +Checks for input length to be within specified limits. + +#### Example + +```html +
    +... + +... +
    +``` + +In the example above: + +* `f` - false +* `fa` - true +* `dfdfdf` - false + +### validate-new-password + +Checks for input to be 6 or more characters. Leading and trailing spaces are ignored. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-no-empty + +Checks that an input is not empty. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-no-html-tags + +Checks that an input does not have HTML tags. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-no-utf8mb4-characters + +Checks that an input does not have characters that would require more than 3 bytes. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-not-negative-number + +Checks for non-negative number input. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-not-number-first + +Checks that an input does not start with a number. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-number + +Checks for a valid number. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-number-range + +Checks for a number to be within a specified range. + +#### Example + +```html +
    +... + +... +
    +``` + +### validate-one-required + +It is supposed to check for radio buttons selection but it always returns true. There is possibly a bug in JS files. + +#### Example + +```html +
    +... +
    + + + +
    +... +
    +``` + +### validate-one-required-by-name + +Checks for a radio button selection. + +#### Example + +```html +
    +... +
    + + + +
    +... +
    +``` + +### validate-optional-datetime + +Validates an optional datetime field. + +#### Example + +```html +
    +... +
    + + + + +
    +... +
    +``` + +### validate-password + +Checks for an input to be 6 or more characters long. Leading and trailing spaces are ignored. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-per-page-value + +Checks for an input to be a specified value from a comma separated field. + +#### Example + +```html +
    +... +
    + + +
    +... +
    +``` + +* `44` - false +* `8` - true + +### validate-per-page-value-list + +Checks for comma separated numbers. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +* `kjkjdf,dfdf` - false +* `1` - true +* `1,3,4,5` - true + +### validate-phoneLax + +Checks for a valid phone number. Formatting may be lax. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-phoneStrict + +Checks for a valid phone number with strict formatting. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-range + +Checks for an input to be within a specified range. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +* `100` - false +* `9` - true + +### validate-required-datetime + +Validates a required datetime field. + +#### Example + +```html +
    +... +
    + + + + +
    +... +
    +``` + +### validate-select + +Checks for a select field to be selected. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-ssn + +Checks for a valid Social Security number. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-state + +Checks for a valid State/Province. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-street + +Checks for a valid street address. It allows only letters (a-z or A-Z), numbers (0-9), spaces and `#`. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-url + +Checks for a valid URL. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-xml-identifier + +Checks for a valid XML-identifier (Ex: something\_1, block5, id-4). + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-zero-or-greater + +Checks that a number is zero or greater in this field. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-zip-international + +Checks for a valid international zip code. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### validate-zip-us + +Checks for a valid US zip code (Ex: 90602 or 90602-1234). + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### vinUS + +Checks for a valid vehicle identification number (VIN). + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` + +### zip-range + +Checks for a zip code to be in the range 902xx-xxxx to 905-xx-xxxx. + +#### Example + +```html +
    +... +
    + +
    +... +
    +``` diff --git a/src/guides/v2.3/get-started/api-security.md b/src/guides/v2.3/get-started/api-security.md new file mode 100644 index 00000000000..8f790eac805 --- /dev/null +++ b/src/guides/v2.3/get-started/api-security.md @@ -0,0 +1,167 @@ +--- +group: web-api +title: API security +functional_areas: + - Integration +--- + +This topic describes best practices for [API security](https://owasp.org/www-project-api-security/). + +## Input limiting + +Imposing restrictions on the size and number of resources that a user can request through an API can help mitigate denial-of-service (DoS) vulnerabilities. By default, the following built-in API rate limiting is available: + +- REST requests containing inputs that represent a list of entities. When enabled, the default maximum is 20 for synchronous requests and 5,000 for asynchronous requests. +- REST and GraphQL queries that allow paginated results can be limited to a maximum number of items per page. When enabled, the default maximum is 300. +- REST queries that allow paginated results can have a default number of items per page imposed. When enabled, the default maximum is 20. + +By default, these input limits are disabled, but you can use the following methods to enable them: + +- Set the values in the [Admin](https://docs.magento.com/user-guide/configuration/services/magento-web-api.html). +- Run the [`bin/magento config:set` command]({{ page.baseurl }}/config-guide/cli/config-cli-subcommands-config-mgmt-set.html#config-cli-config-set). +- Add entries to the [`env.php` file]({{ page.baseurl }}/config-guide/prod/config-reference-configphp.html#system). +- Set [environment variables]({{ page.baseurl }}/config-guide/deployment/pipeline/example/environment-variables.html). + +When input limiting has been enabled, the system uses the default value for each limitation listed above. You can also configure custom values. + +Although some simple examples for configuring these values from the CLI are provided below, all of the values can be [configured per website and per store view]({{ page.baseurl }}/config-guide/cli/config-cli-subcommands-config-mgmt-set.html#config-cli-config-set) in addition to being configurable globally. In addition, these values can also be configured [via `env.php`]({{ page.baseurl }}/config-guide/prod/config-reference-configphp.html#system) +as well as via [environment variables]({{ page.baseurl }}/config-guide/deployment/pipeline/example/environment-variables.html). + +{:.bs-callout-info} +In addition, the Admin provides a configuration setting for limiting session sizes for Admin users and storefront visitors. + +### Enable the input limiting system + +To enable these input limiting features from the Admin, go to **Stores** > Settings > **Configuration** > **Services** > **Web Api Limits** or **GraphQL Input Limits** and set **Enable Input Limits** to **Yes**. + +To enable with the CLI, run one or both of the following commands: + +```bash +bin/magento config:set webapi/validation/input_limit_enabled 1 +``` + +```bash +bin/magento config:set graphql/validation/input_limit_enabled 1 +``` + +### Maximum parameter inputs + +The `EntityArrayValidator` class constructor limits the number of objects that can be given to inputs that represent arrays of objects. For example, the `PUT /V1/guest-carts/{cartId}/collect-totals` endpoint contains the input parameter `additionalData->extension_attributes->gift_messages`, which represents a list of gift message information objects. + +There are four possible input arrays: + +- `additional_data` +- `agreement_ids` +- `gift_messages` +- `custom_attributes` + +```json +{ + "paymentMethod": { + "po_number": "string", + "method": "string", + "additional_data": [ + "string" + ], + "extension_attributes": { + "agreement_ids": [ + "string" + ] + } + }, + "shippingCarrierCode": "string", + "shippingMethodCode": "string", + "additionalData": { + "extension_attributes": { + "gift_messages": [ + { + "gift_message_id": 0, + "customer_id": 0, + "sender": "string", + "recipient": "string", + "message": "string", + "extension_attributes": { + "entity_id": "string", + "entity_type": "string", + "wrapping_id": 0, + "wrapping_allow_gift_receipt": true, + "wrapping_add_printed_card": true + } + } + ] + }, + "custom_attributes": [ + { + "attribute_code": "string", + "value": "string" + } + ] + } +} +``` + +By default, any one of these arrays can include up to 20 items, but you can change this value in the configuration UI via **Stores** > Settings > **Configuration** > **Services** > **Web API Input Limits** > **Input List Limit** or via CLI using the `webapi/validation/complex_array_limit` configuration path. + +### Input limit for REST endpoints + +Some REST endpoints can contain a high number of elements, and developers need a way to set the limit for each endpoint. The limit for a specific REST endpoint can be set in the `webapi.xml` configuration file for synchronous requests and `webapi_async.xml` for asynchronous requests. +To do this, assign a value for the `` attribute within a `` definition. The value for `input-array-size-limit` must be a non-negative integer. + +The following example sets the input limit for the `/V1/some-custom-route` route. +If the route works synchronously, open the `/etc/webapi.xml` configuration file. Otherwise, open `/etc/webapi_async.xml`. +Add the `data` tag with the `input-array-size-limit` attribute to the route configuration. + +```xml + + + + + + + + + + + +``` + +Clear the configuration cache for the changes to take effect. + +```bash +bin/magento cache:clear config +``` + +### Values by default for REST endpoints + +If you need to change the default limits for REST endpoints, then edit the `webapi` section of the `/app/etc/env.php` file as follows: +```conf +[ +//... + 'webapi' => [ + 'sync' => [ + 'default_input_array_size_limit' => , //overrides values for synchronous REST endpoints + ], + 'async' => [ + 'default_input_array_size_limit' => , //overrides values for asynchronous REST endpoints + ], + ] +//... +]; +``` + +### Maximum page size + +The maximum page size setting controls the pagination of various web API responses. By default, the maximum value is `300`. You can change the default in the Admin by selecting **Stores** > Settings > **Configuration** > **Services** > **Web API Input Limits** or **GraphQl Input Limits** > **Maximum Page Size** field. + +[GraphQL security configuration]({{page.baseurl}}/graphql/security-configuration.html) describes how to set the maximum page size in GraphQL. + +### Default page size + +The Default Page Size setting controls the pagination of various web API responses. You can change the default value of `20` in the Admin by selecting **Stores** > Settings > **Configuration** > **Services** > **Web API Input Limits** > **Default Page Size**. To change the value from the CLI, run the following command: + +```shell +bin/magento config:set webapi/validation/default_page_size 30 +``` diff --git a/src/guides/v2.3/get-started/authentication/gs-authentication-oauth.md b/src/guides/v2.3/get-started/authentication/gs-authentication-oauth.md new file mode 100644 index 00000000000..6e070c14b3c --- /dev/null +++ b/src/guides/v2.3/get-started/authentication/gs-authentication-oauth.md @@ -0,0 +1,467 @@ +--- +group: web-api +title: OAuth-based authentication +functional_areas: + - Integration +--- + +Magento OAuth authentication is based on [OAuth 1.0a](https://tools.ietf.org/html/rfc5849), an open standard for secure [API](https://glossary.magento.com/api) authentication. OAuth is a token-passing mechanism that allows a system to control which third-party applications have access to internal data without revealing or storing any user IDs or passwords. + +In Magento, a third-party application that uses OAuth for authentication is called an [_integration_]( {{ page.baseurl }}/get-started/create-integration.html ). An integration defines which resources the application can access. The application can be granted access to all resources or a customized subset of resources. + +As the process of registering the integration proceeds, Magento creates the tokens that the application needs for authentication. It first creates a request token. This token is short-lived and must be exchanged for an access token. Access tokens are long-lived and will not expire unless the merchant revokes access from the application. + +## OAuth overview {#overview} + +The following diagram shows the OAuth authentication process. Each step is described further. +![OAuth flow]({{ page.baseurl }}/get-started/authentication/images/oauthflow.png) + +1. **Create an integration**. The merchant creates an integration from [Admin](https://glossary.magento.com/admin). Magento generates a consumer key and a consumer secret. + +1. **Activate the integration**. The OAuth process begins when the merchant activates the integration. Magento sends the OAuth consumer key and secret, an OAuth verifier, and the store [URL](https://glossary.magento.com/url) to the external application via HTTPS post to the page defined in the **Callback Link** field in Admin. See [Activate an integration](#activate) for more information. + +1. **Process activation information**. The integrator must store the activation information received in step 2. These parameters will be used to ask for tokens. + +1. **Call the application's login page**. Magento calls the page defined in the **Identity Link** field in Admin. + +1. **Merchant logs in to the external application.** If the login is successful, the application returns to the location specified in the call. The login page is dismissed. + +1. **Ask for a request token**. The application uses the `POST /oauth/token/request` REST API to ask for a request token. The `Authorization` header includes the consumer key and other information. See [Get a request token](#pre-auth-token) for details about this token request. + +1. **Send the request token**. Magento returns a request token and request token secret. + +1. **Ask for an access token**. The application uses the `POST /oauth/token/access` REST API to ask for an access token. The `Authorization` header includes the request token and other information. See [Get an access token](#get-access-token) for details about this token request. + +1. **Magento sends the access token**. If this request is successful, Magento returns an access token and access token secret. + +1. **The application can access Magento resources.** All requests sent to Magento must use the full set of request parameters in `Authorization` header. See [Access the web APIs](#web-api-access) for more information. + +## Activate an integration {#activate} + +The integration must be configured from the [Admin](https://glossary.magento.com/magento-admin) (**System > Extensions > Integrations**). The configuration includes a callback URL and an identity link URL. The callback URL specifies where OAuth credentials can be sent when using OAuth for token exchange. The identity link points to the login page of the third-party application that is integrating with Magento. + +A merchant can choose to select **Save and Activate** when the integration is created. Alternatively, the merchant can click on **Activate** against a previously saved integration from the Integration grid. + +When the integration is created, Magento generates a consumer key and a consumer secret. + +Activating the integration submits the credentials to the endpoint specified when creating the Integration. An HTTP POST from Magento to the Integration endpoint will contain these attributes: + +* `store_base_url` For example, `http://my-magento-store.com/`. +* `oauth_verifier` +* `oauth_consumer_key` +* `oauth_consumer_secret` + +Integrations use the `oauth_consumer_key` key to get a request token and the `oauth_verifier` to get an access token. + +## OAuth handshake details {#oauth-handshake} + +The process of completing the OAuth handshake requires that you + +* [Get a request token](#pre-auth-token) +* [Get an access token](#get-access-token) + +This process is known as a 2-legged OAuth handshake. + +### Get a request token {#pre-auth-token} + +A request token is a temporary token that the user exchanges for an access token. Use the following API to get a request token from Magento: + +`POST /oauth/token/request` + +You must include these request parameters in the `Authorization` header in the call: + +Parameter | Description +--- | --- +`oauth_consumer_key` | The consumer key is generated when you create the integration. +`oauth_signature_method` | The name of the signature method used to sign the request. Must be the value `HMAC-SHA1`. +`oauth_signature` | A generated value (signature) +`oauth_nonce` | A random value that is uniquely generated by the application. +`oauth_timestamp` | A positive integer, expressed in the number of seconds since January 1, 1970 00:00:00 GMT. +`oauth_version` | The OAuth version. + +The response contains these fields: + +* `oauth_token`. The token to be used when requesting an access token. +* `oauth_token_secret`. A secret value that establishes ownership of the token. + +A valid response looks like this: + +`oauth_token=4cqw0r7vo0s5goyyqnjb72sqj3vxwr0h&oauth_token_secret=rig3x3j5a9z5j6d4ubjwyf9f1l21itrr` + +### Get an access token {#get-access-token} + +The request token must be exchanged for an access token. Use the following API to get an access token from Magento: + +`POST /oauth/token/access` + +You must include these request parameters in the `Authorization` header in the call: + +Parameter | Description +--- | --- +`oauth_consumer_key` | The consumer key value that you retrieve after you register the integration. +`oauth_nonce` | A random value that is uniquely generated by the application. +`oauth_signature` | A generated value (signature) +`oauth_signature_method` | The name of the signature method used to sign the request. Must be the value `HMAC-SHA1`. +`oauth_timestamp` | A positive integer, expressed in the number of seconds since January 1, 1970 00:00:00 GMT. +`oauth_version` | The OAuth version. +`oauth_token` | The `oauth_token` value, or request token, obtained in [Get a request token](#pre-auth-token). +`oauth_verifier` | The verification code that is tied to the consumer and request token. It is sent as part of the initial POST operation when the integration is activated. + +A valid response looks like this: +`oauth_token=0lnuajnuzeei2o8xcddii5us77xnb6v0&oauth_token_secret=1c6d2hycnir5ygf39fycs6zhtaagx8pd` + +The response contains these fields: + +* `oauth_token`. The access token that provides access to protected resources. +* `oauth_token_secret`. The secret that is associated with the access token. + +## Access the web APIs {#web-api-access} + +After the integration is authorized to make API calls, third-party applications (registered as integrations in Magento) can invoke Magento web APIs by using the access token. + +To use the access token to make [web API](https://glossary.magento.com/web-api) calls: + +`GET /rest/V1/products/1234` + +You must include these request parameters in the `Authorization` request header in the call: + +* `oauth_consumer_key`. The customer key value provided after the registration of the application. +* `oauth_nonce`. A random value, uniquely generated by the application. +* `oauth_signature_method`. The name of the signature method used to sign the request. Valid values are: `HMAC-SHA1`, `RSA-SHA1`, and `PLAINTEXT`. +* `oauth_signature`. A generated value (signature). +* `oauth_timestamp`. A positive integer, expressed in the number of seconds since January 1, 1970 00:00:00 GMT. +* `oauth_token`. The `oauth_token`, or access token, value obtained in [Get an access token](#get-access-token). + +## The OAuth signature {#oauth-signature} + +All OAuth handshake requests and Web Api requests include the signature as part of [Authorization](https://glossary.magento.com/authorization) header. Its generated as follows: + +You concatenate a set of URL-encoded attributes and parameters to construct the signature base string. + +Use the ampersand (`&`) character to concatenate these attributes and parameters: + +1. HTTP method +1. URL +1. `oauth_nonce` +1. `oauth_signature_method` +1. `oauth_timestamp` +1. `oauth_version` +1. `oauth_consumer_key` +1. `oauth_token` + +To generate the signature, you must use the HMAC-SHA1 signature method. The signing key is the concatenated values of the consumer secret and token secret separated by the ampersand (`&`) character (ASCII code 38), even if empty. You must use parameter encoding to encode each value. + +## OAuth token exchange example {#oauth-example} + +The scripts provided in this document simulate the Magento 2 [OAuth 1.0a](https://tools.ietf.org/html/rfc5849) token exchange flow. You can drop these scripts under the document root directory of your Magento application so that they can be exposed as endpoints that your Magento application can interact with to mimic the token exchange. + +The OAuth client is extended from and attributed to [PHPoAuthLib](https://github.com/Lusitanian/PHPoAuthLib), which is the same lib used in the [Magento OAuth client]({{ site.mage2bloburl }}/{{ page.guide_version }}/dev/tests/api-functional/framework/Magento/TestFramework/Authentication/Rest/OauthClient.php). + +To simulate the OAuth 1.0a token exchange flow: + +1. Login to your Admin and navigate to **System > Extensions > Integrations** +1. Click on **Add New Integration**. +1. Complete all details in the Integration Info tab: + * **Name** : SomeUniqueIntegrationName + * **Callback URL** : http://your_app_host/endpoint.php + * **Identity link URL** : http://your_app_host/login.php + * Add permissions as desired on the **API** tab +1. Select the **Save and Activate** option from the drop down menu. +1. A pop-up window displays, confirming API permissions. Click **Allow**. (Make sure your browser allows pop-up windows.) The credentials are posted to `endpoint.php`. You should also see another pop-up for the identity linking step that opens the script from `login.php`. +1. Click **Login**. (There is no actual login check since this is a simulation.). The `checklogin.php` script is called. It uses the posted credentials to complete the token exchange. +1. When the token exchange completes successfully, the user is redirected back to the Integrations grid. The newly-created integration should be in the Active state. +1. Click on the edit icon of the integration and check the Integration Details on the Integration Info tab. It should show all the credentials that can be used to make an authenticated API request using OAuth 1.0. + +### checklogin.php + +{% collapsible Click to expand %} +```php +requestRequestToken(); +$accessToken = $oAuthClient->requestAccessToken( + $requestToken->getRequestToken(), + $oauthVerifier, + $requestToken->getRequestTokenSecret() +); + +header("location: $callback"); +``` +{% endcollapsible %} + +### endpoint.php +{% collapsible Click to expand %} +```php + + +
    + + + + + + + + + + + + + + + + + + + + +
    Integrations Login
    Username:
    Password:
      
    + +
    + + +HTML; +``` +{% endcollapsible %} + +### OauthClient.php + +Change the instances of `http://magento.host` in this example to a valid base URL. + +{% collapsible Click to expand %} + +```php +_parseToken($responseBody); + } + + /** + * Parses the request token response and returns a TokenInterface. + * + * @param string $responseBody + * @return TokenInterface + * @throws TokenResponseException + */ + protected function parseRequestTokenResponse($responseBody) + { + $data = $this->_parseResponseBody($responseBody); + if (isset($data['oauth_verifier'])) { + $this->_oauthVerifier = $data['oauth_verifier']; + } + return $this->_parseToken($responseBody); + } + + /** + * Parse response body and create oAuth token object based on parameters provided. + * + * @param string $responseBody + * @return StdOAuth1Token + * @throws TokenResponseException + */ + protected function _parseToken($responseBody) + { + $data = $this->_parseResponseBody($responseBody); + $token = new StdOAuth1Token(); + $token->setRequestToken($data['oauth_token']); + $token->setRequestTokenSecret($data['oauth_token_secret']); + $token->setAccessToken($data['oauth_token']); + $token->setAccessTokenSecret($data['oauth_token_secret']); + $token->setEndOfLife(StdOAuth1Token::EOL_NEVER_EXPIRES); + unset($data['oauth_token'], $data['oauth_token_secret']); + $token->setExtraParams($data); + return $token; + } + + /** + * Parse response body and return data in array. + * + * @param string $responseBody + * @return array + * @throws \OAuth\Common\Http\Exception\TokenResponseException + */ + protected function _parseResponseBody($responseBody) + { + if (!is_string($responseBody)) { + throw new TokenResponseException("Response body is expected to be a string."); + } + parse_str($responseBody, $data); + if (null === $data || !is_array($data)) { + throw new TokenResponseException('Unable to parse response.'); + } elseif (isset($data['error'])) { + throw new TokenResponseException("Error occurred: '{$data['error']}'"); + } + return $data; + } + + /** + * @override to fix since parent implementation from lib not sending the oauth_verifier when requesting access token + * Builds the authorization header for an authenticated API request + * + * @param string $method + * @param UriInterface $uri the uri the request is headed + * @param \OAuth\OAuth1\Token\TokenInterface $token + * @param $bodyParams array + * @return string + */ + protected function buildAuthorizationHeaderForAPIRequest( + $method, + UriInterface $uri, + TokenInterface $token, + $bodyParams = null + ) { + $this->signature->setTokenSecret($token->getAccessTokenSecret()); + $parameters = $this->getBasicAuthorizationHeaderInfo(); + if (isset($parameters['oauth_callback'])) { + unset($parameters['oauth_callback']); + } + + $parameters = array_merge($parameters, ['oauth_token' => $token->getAccessToken()]); + $parameters = array_merge($parameters, $bodyParams); + $parameters['oauth_signature'] = $this->signature->getSignature($uri, $parameters, $method); + + $authorizationHeader = 'OAuth '; + $delimiter = ''; + + foreach ($parameters as $key => $value) { + $authorizationHeader .= $delimiter . rawurlencode($key) . '="' . rawurlencode($value) . '"'; + $delimiter = ', '; + } + + return $authorizationHeader; + } +} +``` + +{% endcollapsible %} + +{:.ref-header} +Related topics + +[Create an integration]( {{ page.baseurl }}/get-started/create-integration.html ) + +[OAuth error codes]( {{ page.baseurl }}/get-started/authentication/oauth-errors.html ) + +[Construct a request]( {{ page.baseurl }}/get-started/gs-web-api-request.html ) + +[Configure services as web APIs]( {{ page.baseurl }}/extension-dev-guide/service-contracts/service-to-web-service.html ) diff --git a/src/guides/v2.3/get-started/authentication/gs-authentication-session.md b/src/guides/v2.3/get-started/authentication/gs-authentication-session.md new file mode 100644 index 00000000000..513c0e37c2e --- /dev/null +++ b/src/guides/v2.3/get-started/authentication/gs-authentication-session.md @@ -0,0 +1,34 @@ +--- +group: web-api +subgroup: 40_Authentication +title: Session-based authentication +menu_title: Session-based authentication +menu_order: 4 +functional_areas: + - Integration +--- + +As a customer, you log in to the Magento [storefront](https://glossary.magento.com/storefront) with your customer credentials. As an admin, you log in to the [Admin](https://glossary.magento.com/magento-admin) with your [admin](https://glossary.magento.com/admin) credentials. + +The Magento web [API](https://glossary.magento.com/api) framework uses your logged-in session information to verify your identity and authorize access to the requested resource. + +Customers can access resources that are configured with `anonymous` or `self` permission in the `webapi.xml` configuration file. + +Admins can access resources that are assigned to their Admin profile. + + {:.bs-callout-info} +The Magento [web API](https://glossary.magento.com/web-api) framework enables guest users to access resources that are configured with `anonymous` permission. Any user that the framework cannot authenticate through existing authentication mechanisms is considered a guest user. + +For example, if a customer is logged in to the Magento storefront and the [JavaScript](https://glossary.magento.com/javascript) [widget](https://glossary.magento.com/widget) invokes the `self` API, details for the logged-in customer are fetched: + +`GET /rest/V1/customers/me` + +Similarly, if an admin is logged in to the Admin and the JavaScript widget invokes the `Magento_Customer::group` API, details for the logged-in admin are fetched. The web API framework establishes the identity of the admin user based on logged-in session information and authorizes access to the `Magento_Customer::group` resource. + +{:.bs-callout-warning} +Admin session-based authentication is not currently possible for API endpoints. +The session based authentication functionality is restricted to AJAX calls. Direct browser requests cannot be made due to security vulnerabilities. A developer can create a custom storefront widget that can issue requests without additional authentication steps. + +## Related topic + +[Configure services as web APIs]({{ page.baseurl }}/extension-dev-guide/service-contracts/service-to-web-service.html) diff --git a/src/guides/v2.3/get-started/authentication/gs-authentication-token.md b/src/guides/v2.3/get-started/authentication/gs-authentication-token.md new file mode 100644 index 00000000000..fa864a93a11 --- /dev/null +++ b/src/guides/v2.3/get-started/authentication/gs-authentication-token.md @@ -0,0 +1,121 @@ +--- +group: web-api +title: Token-based authentication +functional_areas: + - Integration +--- + +To make a web [API](https://glossary.magento.com/api) call from a client such as a mobile application, you must supply an *access token* on the call. The token acts like an electronic key that lets you access the API. + +Magento issues the following types of access tokens: + +Token type | Description | Default lifetime +--- | --- | --- +Integration | The merchant determines which Magento resources the integration has access to. | Indefinite. It lasts until it is manually revoked. +Admin | The merchant determines which Magento resources an admin user has access to. | 4 hours +Customer | Magento grants access to resources with the `anonymous` or `self` permission. Merchants cannot edit these settings. | 1 hour + +## Integration tokens + +When a merchant creates and activates an integration, Magento generates a consumer key, consumer secret, access token, and access token secret. All of these entities are used for [OAuth-based authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html), but token-based authentication requires only the access token. + +Use the following steps to generate an access token: + +1. Log in to Admin and click **System** > **Extensions** > **Integrations** to display the Integrations page. +1. Click **Add New Integration** to display the New Integration page. +1. Enter a unique name for the integration in the **Name** field. Then enter your admin password in the **Your Password** field. Leave all other fields blank. +1. Click the API tab. Select the Magento resources the integration can access. You can select all resources, or select a custom list. +1. Click **Save** to save your changes and return to the Integrations page. +1. Click the **Activate** link in the grid that corresponds to the newly-created integration. +1. Click **Allow** . A dialog similar to the following displays: + + ![REST client]({{ page.baseurl }}/get-started/authentication/images/integration-tokens.png) + +The access token can be used in all calls made on behalf of the integration. + +## Admin and customer access tokens + +Magento provides a separate token service for administrators and customers. When you request a token from one of these services, the service returns a unique access token in exchange for the username and password for a Magento account. + +The Magento web API framework allows *guest users* to access resources that are configured with the permission level of anonymous. Guest users are users who the framework cannot authenticate through existing authentication mechanisms. As a guest user, you do not need to, but you can, specify a token in a web API call for a resource with anonymous permission. [Restricting access to anonymous web APIs]({{ page.baseurl }}/rest/anonymous-api-security.html) contains a list of APIs that do not require a token. + +Use the following calls to get an authentication token: + +Request|REST|SOAP +---|---|--- +Get an admin token | `POST /V1/integration/admin/token` | `integrationAdminTokenServiceV1` +Get a customer token | `POST /V1/integration/customer/token` | `integrationCustomerTokenServiceV1` + +For most [web API](https://glossary.magento.com/web-api) calls, you supply this token in the `Authorization` request header with the `Bearer` HTTP [authorization](https://glossary.magento.com/authorization) scheme to prove your identity. By default, an admin token is valid for 4 hours, while a customer token is valid for 1 hour. You can change these values from Admin by selecting **Stores** > **Settings** > **Configuration** > **Services** > **OAuth** > **Access Token Expiration**. + +A cron job that runs hourly removes all expired tokens. + +## Request a token {#request-token} + +A access token request contains three basic elements: + +Component | Specifies +--- | --- +Endpoint | A combination of the _server_ that fulfills the request, the web service, and the `resource` against which the request is being made.

    For example, in the `POST /rest//V1/integration/customer/token` endpoint:
    The server is `magento.host/index.php/`,
    the web service is `rest`.
    and the resource is `/V1/integration/customer/token`. +Content type | The content type of the request body. Set this value to either `"Content-Type:application/json"` or `"Content-Type:application/xml"`. +Credentials | The username and password for a Magento account.

    To specify these credentials in a JSON request body, include code similar to the following in the call:

    `{"username":";", "password":""}`

    To specify these credentials in XML, include code similar to the following in the call:

    `customer1customer1pw` + +### Examples {#token-example} + +The following image shows a token request for the [admin](https://glossary.magento.com/admin) account using a REST client: + +![REST client]({{ page.baseurl }}/get-started/authentication/images/gs_auth_token1.png) + +The following example uses the `curl` command to request a token for a customer account: + +```bash +curl -X POST "https://magento.host/index.php/rest/V1/integration/customer/token" \ + -H "Content-Type:application/json" \ + -d '{"username":"customer@example.com", "password":"customer_password"}' +``` + +The following example makes the same request with [XML](https://glossary.magento.com/xml) for a customer account token: + +```bash +curl -X POST "http://magento.vg/index.php/rest/V1/integration/customer/token" \ + -H "Content-Type:application/xml" \ + -d "customer1customer1pw" +``` + +For more information about the `curl` command, see [Use cURL to run the request]({{ page.baseurl }}/get-started/gs-curl.html) + +## Authentication token response {#auth-response} + +A successful request returns a response body with the token, as follows: + +`asdf3hjklp5iuytre` + +## Use the token in a Web API request {#web-api-access} + +Any web API call that accesses a resource that requires a permission level higher than anonymous must contain the authentication token in the header To do this, specify a HTTP header in the following format: + +`Authorization: Bearer ` + +### Admin access {#admin-access} + +Admins can access any resources for which they are authorized. + +For example, to make a web API call with an admin token: + +`curl -X GET "http://magento.ll/index.php/rest/V1/customers/2" -H "Authorization: Bearer vbnf3hjklp5iuytre"` + +### Customer access + +Customers can access only resources with `self` permissions. + +For example, to make a web API call with a customer token: +`curl -X GET "http://magento.ll/index.php/rest/V1/customers/me" -H "Authorization: Bearer asdf3hjklp5iuytre"` + +{:.ref-header} +Related topics + +[Construct a request]({{ page.baseurl }}/get-started/gs-web-api-request.html) + +[Configure services as web APIs]({{ page.baseurl }}/extension-dev-guide/service-contracts/service-to-web-service.html) + +[Restricting access to anonymous web APIs]({{ page.baseurl }}/rest/anonymous-api-security.html) diff --git a/src/guides/v2.3/get-started/authentication/gs-authentication.md b/src/guides/v2.3/get-started/authentication/gs-authentication.md new file mode 100644 index 00000000000..fa04092f489 --- /dev/null +++ b/src/guides/v2.3/get-started/authentication/gs-authentication.md @@ -0,0 +1,214 @@ +--- +group: web-api +title: Authentication +functional_areas: + - Integration +--- + +Magento allows developers to define web [API](https://glossary.magento.com/api) resources and their permissions in the `webapi.xml` configuration file. See [Services as Web APIs]({{ page.baseurl }}/extension-dev-guide/service-contracts/service-to-web-service.html). + +Before you can make [web API](https://glossary.magento.com/web-api) calls, you must authenticate your identity and have necessary permissions (authorization) to access the API resource. Authentication allows Magento to identify the caller's user type. A user's (administrator, integration, customer, or guest) access rights determine an API call's resource accessibility. + +## Accessible resources + +The list of resources that you can access depends on your user type. All customers have the same permissions, and as a result the same resources accessible. The preceding statement is true for guest users as well. +Each administrator or integration user can have a unique set of permissions which is configured in the [Admin](https://glossary.magento.com/magento-admin). +Permissions required to access particular resource are configured in the `webapi.xml` file. This table lists the resources that each user type can access: + +User type | Accessible resources (defined in webapi.xml) +--- | --- +Administrator or Integration | Resources for which administrators or integrators are authorized. For example, if administrators are authorized for the `Magento_Customer::group` resource, they can make a `GET /V1/customerGroups/:id` call. +Customer | Resources with `anonymous` or `self` permission +Guest user | Resources with `anonymous` permission + +## Relationship between acl.xml and webapi.xml + +The `acl.xml` file defines the access control list (ACL) for a given [module](https://glossary.magento.com/module). It defines the available set of permissions to access resources. + +All `acl.xml` files across all Magento modules are consolidated to build an ACL tree, which is used to select allowed [Admin](https://glossary.magento.com/admin) role resources or third-party integration access (**System** > **Extension** > **Integration** > **Add New Integration** > **Available APIs**). + +### Sample customer acl.xml + +For example, account management, customer configuration, and customer group resource permissions are defined in the Customer module's [`acl.xml`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Customer/etc/acl.xml). + +When a developer creates the Web API configuration file (webapi.xml), the permissions defined in acl.xml are referenced to create access rights for each API resource. + +### Sample (truncated) customer webapi.xml + +```xml + + + + + + + + +............ +....... +..... + + + + + + + + + + + + + + + + + + + + + + + + + + %customer_id% + + +.......... +..... +... +``` + +For example, in the preceding `webapi.xml` for the customerGroups resource, only a user with `Magento_Customer::group` authorization can `GET /V1/customerGroups/:id`. On the other hand, you can create a customer using `POST /V1/customers` anonymously (or by a guest). + +[Authorization](https://glossary.magento.com/authorization) is granted to either an administrator (or an integration) defined in the Admin with the customer group selected as one of the resources in the ACL tree. + +{:.bs-callout-info} +A guest or anonymous is a special permission that doesn't need to be defined in `acl.xml` (and will not show up in the permissions tree in the Admin). It just indicates that the current resource in `webapi.xml` can be accessed without the need for authentication. +

    +Similarly, self is a special access used if you already have an authenticated session with the system. Self access enables a user to access resources they own. For example, `GET /V1/customers/me` fetches the logged-in customer's details. This is typically useful for JavaScript-based widgets. + +## Web API clients and authentication methods + +You must use a client, such as a mobile application or an external batch job, to access Magento services using web APIs. + +Each type of client has a preferred authentication method. To authenticate, use the authentication method for your preferred client: + + + + + + + + + + + + + + + + + + +
    ClientAuthentication method and process
    +

    Mobile application

    +
    +

    Registered users use token-based authentication to make web API calls using a mobile application. The token acts like an electronic key that provides access to the API(s).

    +
      +
    1. +

      As a registered Magento user, you request a token from the Magento token service at the endpoint that is defined for your user type.

      +
    2. +
    3. +

      The token service returns a unique authentication token in exchange for a username and password for a Magento account.

      +
    4. +
    5. +

      + To prove your identity, specify this token in the Authorization request header on web API calls. +

      +
    6. +
    + +
    +

    Third-party application

    +
    +

    Third-party applications use OAuth-based authentication to access the web APIs.

    +
      +
    1. +

      The third-party Integration registers with Magento.

      +
    2. +
    3. +

      Merchants authorize extensions and applications to access or update store data.

      +
    4. +
    +
    +

    JavaScript widget on the Magento storefront or Admin

    +
    +

    Registered users use session-based authentication to log in to the Magento storefront or Admin.

    +

    A session is identified by a cookie and time out after a period of inactivity. Additionally, you can have a session as a guest user without logging in.

    +
      +
    1. +

      As a customer, you log in to the Magento storefront with your customer credentials. As an administrator, you log in to the Admin with your administrator credentials.

      +
    2. +
    3. +

      The Magento web API framework identifies you and controls access to the requested resource. +

      +
    4. +
    +
    + +The following sections describe tips about which type of authentication method you should use depending on your use case. + +### Token based (Bearer Authentication) + +This method is a good choice for authenticating customers and Admin users in third-party applications that need to make authorized API calls to the Magento store. + +* **Customer Token**—Use this token in applications to authorize specific customer and query data related to that customer (for example, customer details, cart, and orders). +* **Admin Token**—Use this token in applications to authorize an Admin user and access Admin-related APIs. + +[Request a token]({{ page.baseurl }}/get-started/authentication/gs-authentication-token.html#request-token) and then (include it in future requests)({{ page.baseurl }}/get-started/authentication/gs-authentication-token.html#web-api-access). + +{:.bs-callout-info} +You should use this type of authentication mechanism over HTTPS. + +### Integration (Bearer Authentication) + +This method is a good choice for integrating with a third-party system that supports this kind of authentication. You can restrict access to specific resources. + +Magento generates a consumer key, consumer secret, access token, and access token secret when you create an active integration (self activated). + +To use bearer authentication for API requests, you can use an access token. [Create an active integration]({{ page.baseurl }}/get-started/authentication/gs-authentication-token.html#integration-tokens) (self activated) and [use the access token]({{ page.baseurl }}/get-started/authentication/gs-authentication-token.html#web-api-access) in the authorization header: + +```bash +curl -X GET "http://magento2ce74.loc:8080/index.php/rest/V1/customers/1" -H "Authorization: Bearer 9xvitupdkju0cabq2i3dxyg6bblqmg5h" +``` + +{:.bs-callout-info} +You should use this type of authentication mechanism over HTTPS. + +### Integration (Oauth) + +This method is a good choice for integrating with a third-party system that supports OAuth 1.0a. + +After activating an integration (self activated), you can use the generated consumer key, consumer secret, access token, and access token secret to provide third-party systems access to Magento Store resources. You do not need to make calls to the `/oauth/token/request` or `/oauth/token/access` endpoints to exchange tokens. + +If a third-party system provides endpoints, you can use them to [activate an integration]({{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html#activate) and link your account. After completing the activation process, a third-party service can use the consumer key, consumer secret, access token, and access token secret provided by Magento during activation to make API calls. + +{:.ref-header} +Related topics + +Proceed to the authentication method for your preferred client: + +* Mobile application. [Token-based authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-token.html). + +* Third-party application. [OAuth-based authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html). + +* JavaScript [widget](https://glossary.magento.com/widget) on the Admin or [storefront](https://glossary.magento.com/storefront). [Session-based authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +* [Extension attribute authentication]({{ page.baseurl }}/extension-dev-guide/attributes.html) + +* [Configure services as web APIs]({{ page.baseurl }}/extension-dev-guide/service-contracts/service-to-web-service.html) + +* [Create an ACL rule]({{ page.baseurl }}/ext-best-practices/tutorials/create-access-control-list-rule.html) diff --git a/src/guides/v2.3/get-started/authentication/images/gs_auth_token1.png b/src/guides/v2.3/get-started/authentication/images/gs_auth_token1.png new file mode 100644 index 00000000000..4f338d76d37 Binary files /dev/null and b/src/guides/v2.3/get-started/authentication/images/gs_auth_token1.png differ diff --git a/src/guides/v2.3/get-started/authentication/images/integration-tokens.png b/src/guides/v2.3/get-started/authentication/images/integration-tokens.png new file mode 100644 index 00000000000..c1baa219949 Binary files /dev/null and b/src/guides/v2.3/get-started/authentication/images/integration-tokens.png differ diff --git a/src/guides/v2.3/get-started/authentication/images/oauthflow.png b/src/guides/v2.3/get-started/authentication/images/oauthflow.png new file mode 100644 index 00000000000..600b1646a3d Binary files /dev/null and b/src/guides/v2.3/get-started/authentication/images/oauthflow.png differ diff --git a/src/guides/v2.3/get-started/authentication/oauth-errors.md b/src/guides/v2.3/get-started/authentication/oauth-errors.md new file mode 100644 index 00000000000..f94f1dd94df --- /dev/null +++ b/src/guides/v2.3/get-started/authentication/oauth-errors.md @@ -0,0 +1,32 @@ +--- +group: web-api +title: OAuth error codes +functional_areas: + - Integration +--- + +When the third-party application makes an invalid request to Magento, the following OAuth-related errors can occur: + +HTTP code | Error code | Text representation | Description +--- | --- | --- | --- +400 | 1 | `version_rejected` | The `oauth_version` parameter does not correspond to the "1.0" value. +400 | 2 | `parameter_absent` | A required parameter is missing in the request. The name of the missing parameter is specified additionally in the response. +400 | 3 | `parameter_rejected` | The type of the parameter or its value do not meet the protocol requirements (for example, array is passed instead of the string). +400 | 4 | `timestamp_refused` | The timestamp value in the oauth_timestamp parameter is incorrect. +401 | 5 | `nonce_used` | The nonce-timestamp combination has already been used. +400 | 6 | `signature_method_rejected`| The signature method is not supported. The following methods are supported: HMAC-SHA1. +401 | 7 | `signature_invalid` | The signature is invalid. +401 | 8 | `consumer_key_rejected` | The Consumer Key has incorrect length or does not exist. +401 | 9 | `token_used` | An attempt of authorization of an already authorized token or an attempt to exchange a not temporary token for a permanent one. +401 | 10 | `token_expired` | The temporary token has expired. At the moment, the mechanism of expiration of temporary tokens is not implemented and the current error is not used. +401 | 11 | `token_revoke` | The token is revoked by the user who authorized it. +401 | 12 | `token_rejected` | The token is not valid, or does not exist, or is not valid for using in the current type of request. +401 | 13 | `verifier_invalid` |The confirmation string does not correspond to the token. +403 | 14 | `permission_unknown` |The consumer permission is unknown. +403 | 15 | `permission_denied` |The consumer does not authorized to access the resource. +405 | 16 | `method_not_allowed` |The method is not supported or not allowed. +403 | 17 | `consumer_key_invalid` |The Consumer Key is invalid. + +## Related topic + +[OAuth-based authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html) diff --git a/src/guides/v2.3/get-started/bk-get-started-api.md b/src/guides/v2.3/get-started/bk-get-started-api.md new file mode 100644 index 00000000000..b0e7a1c3e28 --- /dev/null +++ b/src/guides/v2.3/get-started/bk-get-started-api.md @@ -0,0 +1,54 @@ +--- +group: web-api +subgroup: 01_Introduction +title: Getting Started with our Web APIs +landing-page: Getting Started with our APIs +menu_title: Introduction +menu_order: 1 +menu_node: parent +functional_areas: + - Integration +--- + +## What are the Magento web APIs? {#whatare} + +The Magento web [API](https://glossary.magento.com/api) framework provides integrators and developers the means to use web services that communicate with the Magento system. Key features include: + +* Support for [GraphQL]({{page.baseurl}}/graphql/index.html), [REST]({{ page.baseurl }}/rest/bk-rest.html) (Representational State Transfer) and [SOAP]({{ page.baseurl }}/soap/bk-soap.html) (Simple Object Access Protocol). In Magento 2, the [web API](https://glossary.magento.com/web-api) coverage is the same for both REST and SOAP. + +* Three types of [authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication.html): + * Third-party applications authenticate with [OAuth 1.0a]({{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html). + * Mobile applications authenticate using [tokens]({{ page.baseurl }}/get-started/authentication/gs-authentication-token.html). + * Administrators and customers are authenticated with [login credentials]({{ page.baseurl }}/get-started/authentication/gs-authentication-token.html). + +* All accounts and integrations are assigned resources that they have access to. The API framework checks that any call has the [authorization](https://glossary.magento.com/authorization) to perform the request. + +* Any Magento or third-party service can be [configured as a web API]({{ page.baseurl }}/extension-dev-guide/service-contracts/service-to-web-service.html) with a few lines of [xml](https://glossary.magento.com/xml). To configure a web API, you define XML elements and attributes in a `webapi.xml` configuration file. If a service is not defined in a configuration file, it will not be exposed at all. + +* The framework is based on the CRUD (create, read, update, delete) & search model. The system does not currently support webhooks. + +* The framework supports field filtering of web API responses to conserve mobile bandwidth. + +* Integration style web APIs enable a single web API call to run multiple services at once for a more efficient integration. An example of this behavior can be seen in the [Catalog](https://glossary.magento.com/catalog) where one web API call can create a product. If your payload includes the `stock_item` and `media_gallery_entries` objects, then the framework will also create the product’s inventory & media in that one API call. + +## What can I do with the Magento web APIs? {#uses} + +The APIs can be used to perform a wide array of tasks. For example: + +* Create a shopping app. This can be a traditional app that a user downloads on a mobile device. You could also create an app that an employee uses on a showroom floor to help customers make purchases. + +* Integrate with CRM (Customer Relationship Management) or ERP (Enterprise Resource Planning) backend systems, such as Salesforce or Xero. + +* Integrate with a [CMS](https://glossary.magento.com/cms) (Content Management System). Currently, content tagging is not supported. + +* Create [JavaScript](https://glossary.magento.com/javascript) widgets in the Magento [storefront](https://glossary.magento.com/storefront) or on the [Admin](https://glossary.magento.com/admin) panel. The [widget](https://glossary.magento.com/widget) makes AJAX calls to access services. + +## How do I get started? {#procedure} + +You must register a web service on [Admin](https://glossary.magento.com/magento-admin). Use the following general steps to set up Magento to enable web services. + +1. If you are using token-based authentication, create a web services user on Admin by selecting **System** > Permission > **All Users** > Add New User. (If you are using session-based or OAuth authentication, you do not need to create the new user in the Admin.) +1. Create a new integration on Admin. To create an integration, click **System** > Extensions > **Integration** > Add New Integration**. Be sure to restrict which resources the integration can access. +1. Use a REST or SOAP client to configure authentication. + +See the User Guide for more information. diff --git a/src/guides/v2.3/get-started/create-integration.md b/src/guides/v2.3/get-started/create-integration.md new file mode 100644 index 00000000000..4fc3b9eb470 --- /dev/null +++ b/src/guides/v2.3/get-started/create-integration.md @@ -0,0 +1,337 @@ +--- +group: web-api +subgroup: Web APIs +title: Create an integration +menu_title: Create an integration +menu_order: 1 +--- + +An **integration** enables third-party services to call the Magento web APIs. The Magento APIs currently supports Accounting, Enterprise Resource Planning (ERP), Customer Relationship Management (CRM), Product Information Management (PIM), and marketing automation systems out of the box. + +Implementing a simple integration requires little knowledge of [PHP](https://glossary.magento.com/php) or Magento internal processes. However, you will need a working knowledge of + +* [Magento REST or SOAP Web APIs]({{ page.baseurl }}/get-started/bk-get-started-api.html) +* [Web API authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication.html) +* [OAuth-based authentication]( {{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html ) + +Before you begin creating a module, make sure that you have a working installation of Magento 2.0, and the [Magento System Requirements]({{ page.baseurl }}/install-gde/system-requirements.html). + +To create an integration, follow these general steps: + +1. [Create a module with the minimal structure and configuration.](#skeletal) +1. [Add files specific to the integration.](#files) +1. [Install the module.](#install) +1. [Check the integration.](#check) +1. [Integrate with your application.](#integrate) + +## Create a skeletal module {#skeletal} + +To develop a module, you must: + +1. **Create the module file structure.** The module for an integration, like any other of your custom modules, should be placed under `/app/code/app/code//`. E.g. `/app/code/Vendor1/Module1` + + Also create `etc`, `etc/integration`, and `Setup` subdirectories under `/app/code//`, as shown in the following example: + + ```bash + cd + ``` + + ```bash + mkdir -p app/code///etc/integration + ``` + + ```bash + mkdir -p app/code///Setup + ``` + + For more detailed information, see [Create your component file structure]({{ page.baseurl }}/extension-dev-guide/build/module-file-structure.html). + +1. **Define your module configuration file.** The `etc/module.xml` file provides basic information about the module. Change directories to the `etc` directory and create the `module.xml` file. You must specify values for the following attributes: + + + + + + + + + + + + + +
    AttributeDescription
    nameA string that uniquely identifies the [module](https://glossary.magento.com/module).
    setup_versionThe version of Magento the component uses
    + The following example shows an example `etc/module.xml` file. + + ```xml + + + + + + + + + ``` + + Module `Magento_Integration` is added to "sequence" to be loaded first. It helps to avoid the issue, when a module with integration config loaded, that leads to a malfunction. + +1. **Add your module's `composer.json` file.** Composer is a dependency manager for PHP. You must create a `composer.json` file for your module so that Composer can install and update the libraries your module relies on. Place the `composer.json` file in the `module-` directory. + + The following example demonstrates a minimal `composer.json` file. + + ```json + { + "name": "Vendor1_Module1", + "description": "create integration from config", + "require": { + "php": "~7.2.0|~7.3.0", + "magento/framework": "2.0.0", + "magento/module-integration": "2.0.0" + }, + "type": "magento2-module", + "version": "1.0", + "autoload": { + "files": [ "registration.php" ], + "psr-4": { + "Vendor1\\Module1\\": "" + } + } + } + ``` + + For more information, see [Create a component]({{ page.baseurl }}/extension-dev-guide/build/create_component.html). + +1. **Create a `registration.php` file** The `registration.php` registers the module with the Magento system. It must be placed in the module's root directory. + + ```php + integrationManager = $integrationManager; + } + + /** + * @inheritDoc + */ + + public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + $this->integrationManager->processIntegrationConfig(['TestIntegration']); + } + } + ``` + + In the following line + + `$this->integrationManager->processIntegrationConfig(['testIntegration']);` + + `testIntegration` must refer to your `etc/integration/config.xml` file, and the integration name value must be the same. + + The following example demonstrates a minimal `config.xml` file. + + ```xml + + + someone@example.com + https://example.com + https://example.com/identity_link_url + + + ``` + + Also, be sure to change the path after `namespace` for your vendor and module names. + +## Create integration files {#files} + +Magento provides the Integration module, which simplifies the process of defining your integration. This module automatically performs functions such as: + +* Managing the third-party account that connects to Magento. +* Maintaining OAuth authorizations and user data. +* Managing security tokens and requests. + +To customize your module, you must create multiple [XML](https://glossary.magento.com/xml) files and read through others files to determine what resources existing Magento modules have access to. + +The process for customizing your module includes + +* [Define the required resources](#resources) +* [Pre-configure the integration](#preconfig) + +### Define the required resources {#resources} + +The `etc/integration/api.xml` file defines which [API](https://glossary.magento.com/api) resources the integration has access to. + +To determine which resources an integration needs access to, review the permissions defined in each module's `etc/acl.xml` file. + +In the following example, the test integration requires access to the following resources in the Sales module: + +```xml + + + + + + + + + + + + + + + +``` + +### Pre-configure the integration {#preconfig} + +Your module can optionally provide values in configuration file `config.xml`, so that the integration can be automatically pre-configured with default values. To enable this feature, update the `config.xml` file in the `etc/integration` directory. + +{:.bs-callout-info} +If you pre-configure the integration, the values cannot be edited from the [admin](https://glossary.magento.com/admin) panel. + +The file defines which API resources the integration has access to. + +```xml + + + + + + + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ElementDescription
    integrationsContains one or more integration definitions.
    integration name=""Defines an integration. The name must be specified.
    emailAn email to associate with this integration.
    endpoint_url

    Optional. The [URL](https://glossary.magento.com/url) where OAuth credentials can be sent when using OAuth for token exchange. We strongly recommend using https://.

    +

    See OAuth-based authentication for details.

    identity_link_urlOptional. The URL that redirects the user to link their 3rd party account with the Magento integration.
    + +## Install your module {#install} + +Use the following steps to install your module: + +1. Run the following command to update the Magento [database schema](https://glossary.magento.com/database-schema) and data. + + ```bash + bin/magento setup:upgrade + ``` + +1. Run the following command to generate the new code. + + {:.bs-callout-info} + In Production mode, you may receive a message to 'Please rerun Magento compile command'. Enter the command below. Magento does not prompt you to run the compile command in Developer mode. + + ```bash + bin/magento setup:di:compile + ``` + +1. Run the following command to clean the cache. + + ```bash + bin/magento cache:clean + ``` + +## Check your integration {#check} + +Log in to Magento and navigate to **System > Extensions > Integrations**. The integration should be displayed in the grid. + +## Integrate with your application {#integrate} + +Before you can activate your integration in Magento, you must create two pages on your application to handle OAuth communications. + +* The location specified in the `identity_link_url` parameter must point to a page that can handle login requests. + +* The location specified in the `endpoint_url` parameter (**Callback URL** in Admin) must be able to process OAuth token exchanges. + +### Login page {#login} + +When a merchant clicks the **Activate** button in Admin, a pop-up login page for the third-party application displays. Magento sends values for `oauth_consumer_key` and `success_call_back` parameters. The application must store the value for `oauth_consumer_key` to tie it to the login ID. Use the `success_call_back` parameter to return control back to Magento. + +### Callback page {#callback} + +The callback page must be able to perform the following tasks: + +* Receive an initial HTTPS POST that Magento sends when the merchant activates integration. This post contains the Magento store URL, an `oauth_verifier`, the OAuth consumer key, and the OAuth consumer secret. The consumer key and secret are generated when the integration is created. + +* Ask for a request token. A request token is a temporary token that the user exchanges for an access token. Use the following API to get a request token from Magento: + + `POST /oauth/token/request` + + See [Get a request token]( {{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html#pre-auth-token ) for more details about this call. + +* Parse the request token response. The response contains an `oauth_token` and `oauth_token_secret`. + +* Ask for an access token. The request token must be exchanged for an access token. Use the following API to get a request token from Magento: + + `POST /oauth/token/access` + + See [Get an access token]( {{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html#get-access-token ) for more details about this call. + +* Parse the access token response. The response contains an `oauth_token` and `oauth_token_secret`. These values will be different than those provided in the request token response. + +* Save the access token and other OAuth parameters. The access token and OAuth parameters must be specified in the `Authorization` header in each call to Magento. + +## Related Topics + +* [Web API authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication.html) +* [OAuth-based authentication]( {{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html ) +* [Magento System Requirements]({{ page.baseurl }}/install-gde/system-requirements.html) +* [Create the module file structure]({{ page.baseurl }}/extension-dev-guide/build/module-file-structure.html) +* [Create a component]({{ page.baseurl }}/extension-dev-guide/build/create_component.html) diff --git a/src/guides/v2.3/get-started/gs-curl.md b/src/guides/v2.3/get-started/gs-curl.md new file mode 100644 index 00000000000..8960408aa2c --- /dev/null +++ b/src/guides/v2.3/get-started/gs-curl.md @@ -0,0 +1,273 @@ +--- +group: web-api +title: Use cURL to run the request +functional_areas: + - Integration +--- + +['cURL'](https://curl.haxx.se/) is a command-line tool that lets you transmit HTTP requests and receive responses from the command line or a shell script. It is available for Linux distributions, Mac OS X, and Windows. + +To use cURL to run your REST web [API](https://glossary.magento.com/api) call, use the cURL command syntax to construct the command. + +To create the endpoint in the call, append the REST URI that you constructed in [Construct a request]({{ page.baseurl }}/get-started/gs-web-api-request.html) to this URL: + +`https:////rest/` + +To pass the customer data object in the POST call payload, specify a JSON or [XML](https://glossary.magento.com/xml) request body on the call. + +For a complete list of cURL command options, see [curl.1 the man page](https://curl.se/docs/manpage.html). + +The cURL examples in this guide use the following command-line options: + +Option | Description +--- | --- +`-d` `-data` | Sends the specified data in a POST request to the HTTP server. Use this option to send a JSON or XML request body to the server. +`-H` `-header` | Specifies an extra HTTP header in the request. Precede each header with the `-H` option. You can specify any number of extra headers. For a list of common headers used in Magento web API requests, see [HTTP headers]({{ page.baseurl }}/get-started/gs-web-api-request.html#http-headers) +`-i` `-input` | Includes the HTTP header in the output. +`-s` `-silent` | Specifies silent or quiet mode, which makes cURL mute. Progress and error messages are suppressed. +`-T` `-upload-file` | Transfers the specified local file to the remote URL. +`-X` `-request` | Specifies the request method to use when communicating with the HTTP server. The specified request method is used instead of the default GET method. + +## Using cUrl in Magento + +Magento provides its own service-wrapper for using cURL instead of using the default PHP cURL. The class ``Magento\Framework\HTTP\Client\Curl`` may be used to work with HTTP protocol using cURL library. +First, create an instance of `Magento\Framework\HTTP\Client\Curl`. + +```php +/** +* Constructor. +* +* @param Magento\Framework\HTTP\Client\Curl $curl +*/ +public function __construct( + Magento\Framework\HTTP\Client\Curl $curl +) { + $this->curl = $curl; +} +``` + +### Make GET request using cURL + +```php +// get method +$this->curl->get($url); + +// output of curl request +$result = $this->curl->getBody(); +``` + +where `$url` is the endpoint URL. + +### Make POST request using cURL + +```php +// post method +$this->curl->post($url, $params); + +// output of curl requestt +$result = $this->curl->getBody(); +``` + +where `$url` is the endpoint URL, `$params` is an array of data that is being sent via the POST request. Other parameters may be added in the URL directly. +A `$params` example: + +```php +$params = [ + 'user[email]' => $user->getEmail(), + 'user[cellphone]' => $providerInfo['phone_number'], + 'user[country_code]' => $providerInfo['country_code'], +] +``` + +The cURL client can also add headers, basic authorization, additional cURL options, and cookies in the cURL request. The cURL client provides these methods before using `get` or `post` method. + +### Set cURL header using addHeader method + +The `addHeader` method accepts two parameters. The cURL header name and a cURL header value. + +```php +$this->curl->addHeader("Content-Type", "application/json"); +$this->curl->addHeader("Content-Length", 200); +``` + +### Set cURL header using setHeaders method + +The `setHeaders` method accepts an array as a parameter. + +```php +$headers = ["Content-Type" => "application/json", "Content-Length" => "200"]; +$this->curl->setHeaders($headers); +``` + +### Set basic authorization in cURL + +Set the basic authorization using the `setCredentials` method. + +```php +$userName = "User_Name"; +$password = "User_Password"; + +$this->curl->setCredentials($userName, $password); +``` + +It is equivalent to setting CURLOPT_HTTPHEADER value: + +```php +"Authorization : " . "Basic " . base64_encode($userName . ":" . $password) +``` + +### Set cURL option using setOption method + +The `setOption` method accepts two parameters. The cURL option name and the cURL option value. + +```php +$this->curl->setOption(CURLOPT_RETURNTRANSFER, true); +$this->curl->setOption(CURLOPT_PORT, 8080); +``` + +### Set cURL option using setOptions method + +The `setOptions` method accepts an array as a parameter. + +```php +$options = [CURLOPT_RETURNTRANSFER => true, CURLOPT_PORT => 8080]; + +$this->curl->setOptions($options); +``` + +### Set cURL cookies using addCookie method + +The `addCookie` method accepts an array as a parameter. The cookie name and the cookie value. + +```php +$this->curl->addCookie("cookie-name", "cookie-value"); +``` + +### Set cURL cookies using setCookies method + +The ``setCookies`` method accepts an array as a parameter. + +```php +$cookies = ["cookie-name-1" => "cookie-value-1", "cookie-name-2" => "cookie-value-2"]; +$this->curl->setCookies($cookies); +``` + +### How to use cURL with Magento + +For example, the `Magento\Marketplace\Model\Partners` class gets partners info using cURL from the api of Magento connect. + +```php +namespace Magento\Marketplace\Model; + +use Magento\Framework\HTTP\Client\Curl; +use Magento\Marketplace\Helper\Cache; +use Magento\Backend\Model\UrlInterface; + +/** + * @api + * @since 100.0.2 + */ +class Partners +{ + /** + * @var Curl + */ + protected $curlClient; + + /** + * @var string + */ + protected $urlPrefix = 'https://'; + + /** + * @var string + */ + protected $apiUrl = 'magento.com/magento-connect/platinumpartners/list'; + + /** + * @var \Magento\Marketplace\Helper\Cache + */ + protected $cache; + + /** + * @param Curl $curl + * @param Cache $cache + * @param UrlInterface $backendUrl + */ + public function __construct(Curl $curl, Cache $cache, UrlInterface $backendUrl) + { + $this->curlClient = $curl; + $this->cache = $cache; + $this->backendUrl = $backendUrl; + } + + /** + * @return string + */ + public function getApiUrl() + { + return $this->urlPrefix . $this->apiUrl; + } + + /** + * Gets partners json + * + * @return array + */ + public function getPartners() + { + $apiUrl = $this->getApiUrl(); + try { + $this->getCurlClient()->post($apiUrl, []); + $this->getCurlClient()->setOptions( + [ + CURLOPT_REFERER => $this->getReferer() + ] + ); + $response = json_decode($this->getCurlClient()->getBody(), true); + if ($response['partners']) { + $this->getCache()->savePartnersToCache($response['partners']); + return $response['partners']; + } else { + return $this->getCache()->loadPartnersFromCache(); + } + } catch (\Exception $e) { + return $this->getCache()->loadPartnersFromCache(); + } + } + + /** + * @return Curl + */ + public function getCurlClient() + { + return $this->curlClient; + } + + /** + * @return cache + */ + public function getCache() + { + return $this->cache; + } + + /** + * @return string + */ + public function getReferer() + { + return \Magento\Framework\App\Request\Http::getUrlNoScript($this->backendUrl->getBaseUrl()) + . 'admin/marketplace/index/index'; + } +} +``` + +First off all the cURL client instance is created in `__construct`. +Method `getPartners` uses the cURL client makes POST request using cURL, the `post` method takes the first parameter the URL to the api of Magento connect, second parameter is empty array, then the option `CURLOPT_REFERER` added by `setOptions` method of the cURL client. +As result the script call `getBody` method of the cURL client. + +{:.ref-header} +Related topics + +[Status codes and responses]({{ page.baseurl }}/get-started/gs-web-api-response.html) diff --git a/src/guides/v2.3/get-started/gs-web-api-request.md b/src/guides/v2.3/get-started/gs-web-api-request.md new file mode 100644 index 00000000000..2f7909341b5 --- /dev/null +++ b/src/guides/v2.3/get-started/gs-web-api-request.md @@ -0,0 +1,281 @@ +--- +group: web-api +title: Construct a request +--- + +To configure a web API, developers define some of the elements of each API call in the `/vendor///etc/webapi.xml` file, where `` is your vendor name (for example, `magento`) and `` is your module name (which exactly matches its definition in `composer.json`). For example, the web API for the Customer service is defined in the `/vendor/magento/module-customer/etc/webapi.xml` configuration file. Service data interfaces and builders define the required and optional parameters and the return values for the [API](https://glossary.magento.com/api) calls. + +## Overview {#request-overview} + +The following table and the sections that follow the table describe [web API](https://glossary.magento.com/web-api) call elements: + +Element | Specifies +--- | --- +[HTTP verb](#verbs) | The action to perform against the endpoint. +[Endpoint](#endpoints) | A combination of the _server_ that fulfills a request, the web service, and the _resource_ against which the request is being made. +[HTTP headers](#http-headers) | The authentication token, the call request and response formats, and other information. +[Call payload](#payload) | A set of input parameters and attributes that you supply with the request. API operations have both **required** and **optional** inputs. You specify input parameters in the URI and input attributes in a request body. You can specify a JSON- or XML-formatted request body. + +### HTTP verb {#verbs} + +Specify one of these HTTP verbs in the request: + +* `GET`. Requests transfer of a current representation of the target resource. If you omit the verb, `GET` is the default. +* `PUT`. Requests that the state of the target resource be created or replaced with the state defined by the representation enclosed in the request message payload. +* `POST`. Requests that the origin server accept the representation enclosed in the request as data to be processed by the target resource. +* `DELETE`. Requests that the origin server delete the target resource. + +### Endpoint {#endpoints} + +An endpoint is a combination of the _server_ that fulfills a request, the web service, the store code, the resource against which the request is being made, and any template parameters. + +For example, in the `http://magento.ll/index.php/rest/default/V1/customerGroups/:id` endpoint, the server is `magento.ll/index.php/`, the web service is `rest`, the resource is `/V1/customerGroups`, and the template parameter is `id`. + +A store code can have one of the following values. + +* The store's assigned store code. +* `default`. This is the default value when no store code is provided. +* `all`. This value only applies to endpoints defined in the [CMS](https://glossary.magento.com/cms) and Product modules. If this value is specified, the [API](https://glossary.magento.com/api) call affects all of the merchant's stores. + +### HTTP headers {#http-headers} + + {:.bs-callout-info} +To specify an HTTP header in a cURL command, use the `-H` option. + +Specify one or more of the following HTTP headers in your web API calls: + +HTTP header | Description | Syntax +--- | --- | --- +`Authorization` | Required, except for calls made on behalf of a guest. Specifies the authentication token that proves you as the owner of a Magento account. You specify the token in the `Authorization` request header with the `Bearer` HTTP authorization scheme. | `Authorization: Bearer `

    `` is the authentication token returned by the Magento token service. See [Authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication.html). +`Accept` | Optional. Specifies the format of the response body. Default is `JSON`. | `Accept: application/`

    `` is either `JSON` or `XML`. +`Content-Type` | Required for operations with a request body. Specifies the format of the request body. | `Content-Type:application/`

    `` is either `JSON` or `XML`. +`X-Captcha` | A shopper-entered CAPTCHA value. It is required when a shopper enters a CAPTCHA value on the frontend, unless an integration token is provided. Forms requiring CAPTCHA values are configured at **Stores** > **Configuration** > **Customers** > **Customer Configuration** > **CAPTCHA** > **Forms**. | String + +### Call payload {#payload} + +The call payload is a set of input parameters and attributes that you supply with the request. API operations have both _required_ and _optional_ inputs. + +You specify input parameters in the URI. For example, in the `GET/V1/customers/:customerId` URI, you must specify the `customerId` template parameter. This parameter filters the response by the specified customer ID. + +You specify input attributes in a JSON- or XML-formatted request body. For example, in the `POST /V1/customers` call, you must specify a request body like this: + +```json +{ + "customer": { + "email": "user@example.com", + "firstname": "John", + "lastname": "Doe" + }, + "addresses": [ + { + "defaultShipping": true, + "defaultBilling": true, + "firstname": "John", + "lastname": "Doe", + "region": { + "regionCode": "CA", + "region": "California", + "regionId": 12 + }, + "postcode": "90001", + "street": ["Zoe Ave"], + "city": "Los Angeles", + "telephone": "555-000-00-00", + "countryId": "US" + } + ] +} +``` + +This JSON-formatted request body includes a `customer` object with the customer email, first name, and last name, and customer address information. The information in this request body is used to populate the new customer account. + +## Construct a request {#construct-request} + +This example shows you how to construct a REST web API call to create an account. + +1. Open the [Magento/Customer/etc/webapi.xml]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Customer/etc/webapi.xml) + +1. Find the route element that defines the `createAccount` call: + + ```xml + + + + + + + ``` + +1. Use the method and `url` values on the `route` element to construct the URI. In this example, the URI is POST `/V1/customers`. + +1. Use the `class` attribute on the `service` element to identify the service interface. In this example, the service interface is the `AccountManagementInterface` [PHP](https://glossary.magento.com/php) file. + + Open the [AccountManagementInterface.php]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Customer/Api/AccountManagementInterface.php) file and find the createAccount method, as follows: + + ```php?start_inline=1 + public function createAccount( + \Magento\Customer\Api\Data\CustomerInterface $customer, + $password = null, + $redirectUrl = '' + ) + ``` + + The `createAccount` call requires a `customer` data object. The `password` and `redirectUrl` values are optional. The default `password` value is `null` and the default `redirectUrl` value is blank. + +1. To pass the customer data object in the POST call payload, specify JSON or [XML](https://glossary.magento.com/xml) request body on the call. + +### Customers Search API request example {#customers-search-api-request-example} + +The following example builds a Customers Search request based on search criteria. It returns a list of customers that match given search criteria. + +1. Prepare `Authorization`, `Accept` and `Content-Type` headers to be passed to a request object. Use the [Authorization](https://glossary.magento.com/authorization) token returned by the Magento token service. + + ```php?start_inline=1 + $token = 'token'; + $httpHeaders = new \Zend\Http\Headers(); + $httpHeaders->addHeaders([ + 'Authorization' => 'Bearer ' . $token, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json' + ]); + ``` + +1. Open the [Magento/Customer/etc/webapi.xml]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Customer/etc/webapi.xml) configuration file and find the [CustomerRepositoryInterface]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Customer/Api/CustomerRepositoryInterface.php) interface with the `getList` method. + +1. Set the headers, URI and method to a request object. Use URI `/V1/customers/search` and method `GET` values. Use the `searchCriteria` parameter to complete the Customer Search query. See [searchCriteria usage]({{ page.baseurl }}/rest/performing-searches.html). + + The following example finds customers whose first name contains "ver" or whose last name contains "Costello". + + ```php?start_inline=1 + $request = new \Zend\Http\Request(); + $request->setHeaders($httpHeaders); + $request->setUri('http://magento.ll/rest/V1/customers/search'); + $request->setMethod(\Zend\Http\Request::METHOD_GET); + + $params = new \Zend\Stdlib\Parameters([ + 'searchCriteria' => [ + 'filterGroups' => [ + 0 => [ + 'filters' => [ + 0 => [ + 'field' => 'firstname', + 'value' => '%ver%', + 'condition_type' => 'like' + ], + 1 => [ + 'field' => 'lastname', + 'value' => '%Costello%', + 'condition_type' => 'like' + ] + ] + ] + ], + 'current_page' => 1, + 'page_size' => 10 + ], + ]); + + $request->setQuery($params); + ``` + +1. Prepare a HTTP Curl client object and pass the request object to `Client::send()` method. + + ```php?start_inline=1 + $client = new \Zend\Http\Client(); + $options = [ + 'adapter' => 'Zend\Http\Client\Adapter\Curl', + 'curloptions' => [CURLOPT_FOLLOWLOCATION => true], + 'maxredirects' => 0, + 'timeout' => 30 + ]; + $client->setOptions($options); + + $response = $client->send($request); + ``` + +This request returns a list of all customers in JSON format, as shown below. You can also specify XML format by changing Accept header of the request. + +```json +{ + "items": [ + { + "id": 1, + "group_id": 1, + "default_billing": "1", + "default_shipping": "1", + "created_at": "2017-12-05 09:50:11", + "updated_at": "2018-09-22 06:32:50", + "created_in": "Default Store View", + "dob": "1973-12-15", + "email": "roni_cost@example.com", + "firstname": "Veronica", + "lastname": "Costello", + "gender": 2, + "store_id": 1, + "website_id": 1, + "addresses": [ + { + "id": 1, + "customer_id": 1, + "region": { + "region_code": "MI", + "region": "Michigan", + "region_id": 33 + }, + "region_id": 33, + "country_id": "US", + "street": [ + "6146 Honey Bluff Parkway" + ], + "telephone": "(555) 229-3326", + "postcode": "49628-7978", + "city": "Calder", + "firstname": "Veronica", + "lastname": "Costello", + "default_shipping": true, + "default_billing": true + }, + { + "id": 19, + "customer_id": 1, + "region": { + "region_code": "London ", + "region": "London ", + "region_id": 0 + }, + "region_id": 0, + "country_id": "GB", + "street": [ + "1 Studio 103 The Business Centre 61" + ], + "telephone": "1234567890", + "postcode": "CF24 3DG", + "city": "Tottenham ", + "firstname": "Veronica", + "lastname": "Costello" + } + ], + "disable_auto_group_change": 0 + } + ], + "search_criteria": { + "filter_groups": [ + { + "filters": [ + { + "field": "firstname", + "value": "%ver%", + "condition_type": "like" + } + ] + } + ] + }, + "total_count": 1 +} +``` + +{:.ref-header} +Related topics + +Run the web API call through a [cURL command]({{ page.baseurl }}/get-started/gs-curl.html) or a REST client. diff --git a/src/guides/v2.3/get-started/gs-web-api-response.md b/src/guides/v2.3/get-started/gs-web-api-response.md new file mode 100644 index 00000000000..7362142ba9b --- /dev/null +++ b/src/guides/v2.3/get-started/gs-web-api-response.md @@ -0,0 +1,89 @@ +--- +group: web-api +subgroup: 20_REST +title: Status codes and responses +menu_title: Status codes and responses +menu_order: 3 + +--- + +## REST responses {#rest-responses} + +Each web [API](https://glossary.magento.com/api) call returns a HTTP status code and a response payload. When an error occurs, the response body also returns an error message. + +### HTTP status codes {#http-status-codes} + +Each [web API](https://glossary.magento.com/web-api) call returns an HTTP status code that reflects the result of a request: + +HTTP code | Meaning | Description +--- | --- | --- +200 | Success | The framework returns HTTP 200 to the caller upon success. +400 | Bad Request | If service implementation throws either `Magento_Service_Exception` or its derivative, the framework returns a HTTP 400 with a error response including the service-specific error code and message. This error code could indicate a problem such as a missing required parameter or the supplied data didn't pass validation. +401 | Unauthorized | The caller was not authorized to perform the request. For example, the request included an invalid token or a user with customer permissions attempted to access an object that requires administrator permissions. +403 | Forbidden | Access is not allowed for reasons that are not covered by error code 401. +404 | Not found | The specified REST endpoint does not exist. The caller can try again. +405 | Not allowed | A request was made of a resource using a method that is not supported by that resource. For example, using GET on a form which requires data to be presented via POST, or using PUT on a read-only resource. +406 | Not acceptable | The requested resource is only capable of generating content that is not acceptable according to the Accept headers sent in the request. +500 | System Errors | If service implementation throws any other exception like network errors, database communication, framework returns HTTP 500. + +### Response payload {#response-payload} + +POST, PUT, and GET web API calls return a response payload. This payload is a JSON- or XML-formatted response body. The `Accept: application/` header in the request determines the format of the response body, where `FORMAT` is either `json` or `xml`. + +A successful DELETE call returns `true`. An unsuccessful DELETE call returns a payload similar to the other calls. + +The response payload depends on the call. +For example, a `GET /V1/customers/:customerId` call returns the following payload: + +```json +{ + "customers": { + "customer": { + "email": "user@example.com", + "firstname": "John", + "lastname": "Doe" + }, + "addresses": [ + { + "defaultShipping": true, + "defaultBilling": true, + "firstname": "John", + "lastname": "Doe", + "region": { + "regionCode": "CA", + "region": "California", + "regionId": 12 + }, + "postcode": "90001", + "street": ["Zoe Ave"], + "city": "Los Angeles", + "telephone": "555-000-00-00", + "countryId": "US" + } + ] + } +} +``` + +This JSON-formatted response body includes a `customer` object with the customer email, first name, and last name, and customer address information. The information in this response body shows account information for the specified customer. + +### Error format {#error-format} + +When an error occurs, the response body contains an error code, error message, and optional parameters. + +Part | Description +--- | --- | --- +`code` | The status code representing the error. +`message` | The message explaining the error. +`parameters` | Optional. An array of attributes used to generate a different and/or localized error message for the client. + +As an example, Magento returns a `code` of `400` and the following `message` when an invalid `sku` value is specified in the call `PUT V1/products/:sku`. + +```json +{ + "message": "Invalid product data: %1", + "parameters": [ + "Invalid attribute set entity type" + ] +} +``` diff --git a/src/guides/v2.3/get-started/rest_front.md b/src/guides/v2.3/get-started/rest_front.md new file mode 100644 index 00000000000..0fe53aaa319 --- /dev/null +++ b/src/guides/v2.3/get-started/rest_front.md @@ -0,0 +1,54 @@ +--- +group: web-api +subgroup: 20_REST +title: Use REST APIs +menu_order: 1 +menu_node: parent +--- + +The Magento REST [API](https://glossary.magento.com/api) defines a set of functions that a developer can use to perform requests and receive responses. These interactions are performed using the HTTP protocol. + +The caller issues an HTTP request, which contains the following elements: + +* An HTTP header that provides authentication and other instructions +* A verb, which can be one of GET, POST, PUT, or DELETE. +* An endpoint, which is a Uniform Resource Indicator (URI) that identifies the server, the web service, and the resource being acted on. +* The call payload, which is set of input parameters and attributes that you supply with the request. + +Magento returns a response payload as well as an HTTP status code. + +This guide introduces web API, REST, and cURL command concepts. It shows you how to authenticate and construct and run REST [web API](https://glossary.magento.com/web-api) calls. You run REST web API calls through cURL commands or a REST client. + +Read the following sections to get up and running with the Magento web APIs: + + + + diff --git a/src/guides/v2.3/get-started/soap/soap-web-api-calls.md b/src/guides/v2.3/get-started/soap/soap-web-api-calls.md new file mode 100644 index 00000000000..f7d7178a4f7 --- /dev/null +++ b/src/guides/v2.3/get-started/soap/soap-web-api-calls.md @@ -0,0 +1,96 @@ +--- +group: web-api +subgroup: 30_SOAP +title: Use SOAP Services +menu_title: Use SOAP Services +menu_order: 1 +menu_node: parent +--- + +## WSDL File {#wsdl} + +A WSDL file is generated only for services that you request. This means that different clients may use different services and therefore use different WSDLs. + +The Magento web [API](https://glossary.magento.com/api) uses WSDL 1.2, which complies with WS-I 2.0 Basic Profile. + +Each Magento service interface that is part of a [service contract](https://glossary.magento.com/service-contract) is represented as a separate service in the WSDL. + +To consume several services, you must specify them in the WSDL endpoint [URL](https://glossary.magento.com/url). + +| Service | WSDL endpoint URL | Available services | +| --------- | ---------- | ------------------------------------------ | +| customer | http://magentohost/soap?wsdl&services=customerCustomerRepositoryV1 | \Magento\Customer\Api\Data\CustomerInterface | +| customer, catalogProduct | http://magentohost/soap/custom_store?wsdl&services=customerCustomerRepositoryV1,catalogProductRepositoryV1 | \Magento\Customer\Api\Data\CustomerInterface, \Magento\Catalog\Api\Data\ProductInterface | + +The WSDL URL follows the following pattern: + +`http:///soap/?wsdl&services=,` + +You must specify each service version in the endpoint URL. + +This way, you can have a strict contract between your application and the service provider. + +If you want an overview to all the available Web Services, use the following URL format to get a list of all SOAP Services: + +`http:///soap/all?wsdl_list=1` + +```xml + + ... + + http:///soap/all?wsdl&services=storeStoreRepositoryV1 + + + http:///soap/all?wsdl&services=storeGroupRepositoryV1 + + + http:///soap/all?wsdl&services=storeWebsiteRepositoryV1 + + ... + +``` + +### Service class-to-service name conversion rules + +Service names use the following conventions: + +* CamelCase is used for service naming. +* The string `Service` is omitted. +* The `Magento` prefix is omitted. +* The `Interface` suffix is omitted. +* If the service name is the same as the [module](https://glossary.magento.com/module) name, the module name is omitted. For example, if there is a customer service interface in the customer module, the word `customer` will be used in the service name only once. + +| Original Service Interface Name | Service Name | +|---------- +| \Magento\Customer\Api\Data\CustomerInterface | customerCustomerRepositoryV1 | +| \Magento\Customer\Api\AccountManagementInterface | customerAccountManagementV1 | +| \Enterprise\Customer\Service\V3\Customer\AddressInterface | enterpriseCustomerAddressV3 | + +## Authentication {#auth} + +Protected SOAP resources can be accessed using bearer tokens (OAuth access tokens) over HTTP. Access tokens are strings representing an access [authorization](https://glossary.magento.com/authorization) issued to the client. For more information, see [OAuth-based authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html) + +The following [PHP](https://glossary.magento.com/php) script illustrates how to get an access token: + +```php + [ + 'header' => 'Authorization: Bearer 36849300bca4fbff758d93a3379f1b8e' + ] + ]; +$wsdlUrl = 'http://magento.ll/soap/default?wsdl=1&services=testModule1AllSoapAndRestV1'; +$serviceArgs = ["id" => 1]; + +$context = stream_context_create($opts); +$soapClient = new SoapClient($wsdlUrl, ['version' => SOAP_1_2, 'stream_context' => $context]); + +$soapResponse = $soapClient->testModule1AllSoapAndRestV1Item($serviceArgs); ?> +``` + +{:.ref-header} +Related topics + +* [OAuth-based authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-oauth.html) +* [Service contracts]({{ page.baseurl }}/extension-dev-guide/service-contracts/service-contracts.html) +* [SOAP Reference]({{ page.baseurl }}/soap/bk-soap.html) diff --git a/src/guides/v2.3/get-started/web-api-functional-testing.md b/src/guides/v2.3/get-started/web-api-functional-testing.md new file mode 100644 index 00000000000..696f9eedf7b --- /dev/null +++ b/src/guides/v2.3/get-started/web-api-functional-testing.md @@ -0,0 +1,120 @@ +--- +group: web-api +title: Web API functional testing +--- + +The Web [API](https://glossary.magento.com/api) testing framework allows you to test Magento [Web API](https://glossary.magento.com/web-api) from the client application point of view. The tests can be used with either REST or SOAP. The REST or SOAP [adapter](https://glossary.magento.com/adapter) that runs the tests is specified in PHPUnit configuration. See [How to Run the Tests](#howto) for more information. + +To run Web API tests for GraphQl, see [GraphQL functional testing]({{ page.baseurl }}/graphql/functional-testing.html). + +## Implementation Details {#details} + +The Web API functional testing framework depends on the integration testing framework and reuses most of classes implemented there. + +### Custom Annotations for Data Fixtures {#custom} + +In the Web API functional tests only, the custom annotation `@magentoApiDataFixture` is available for declaring fixtures. The difference of this annotation from `@magentoDataFixture` is that the fixture will be committed and accessible during HTTP requests made within the test body. The usage rules of `@magentoApiDataFixture` are the same as `@magentoDataFixture` usage rules. + +{:.bs-callout-tip} +If data was added to the DB using `@magentoApiDataFixture`, it will not be automatically cleared after test execution. The data is cleared when `@magentoDataFixture` is used. + +Do not define fixtures in `dev/tests/api-functional`. Instead, they must be taken from `dev/tests/integration`. The integration framework defines most necessary fixtures, and they should be reused during Web API functional testing. If the existing set of fixtures is insufficient, add new fixtures under `dev/tests/integration`. The fixtures will then be available for both testing frameworks. + +To keep your test environment clean, clear all entities created in fixture files or within tests itself from the DB after test execution. This can be done either directly in tearDown or by a corresponding rollback for the fixture file. This file should be named the same as a fixture, but with `_rollback` suffix. + +## How to Create a New Test {#create} + +All Web API functional tests should inherit from the generic test case `Magento\TestFramework\TestCase\WebapiAbstract`. It defines the `_webApiCall()` method, which should be used to perform Web API calls from tests. Clients of `_webApiCall()` are unaware of which adapter will be used to perform the remote call. + +```php +namespace Magento\Webapi\Routing; + +class CoreRoutingTest extends \Magento\TestFramework\TestCase\WebapiAbstract +{ + public function testBasicRoutingExplicitPath() + { + $itemId = 1; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/testmodule1/' . $itemId, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + ], + 'soap' => [ + 'service' => 'testModule1AllSoapAndRestV1', + 'operation' => 'testModule1AllSoapAndRestV1Item', + ], + ]; + $requestData = ['itemId' => $itemId]; + $item = $this->_webApiCall($serviceInfo, $requestData); + $this->assertEquals('testProduct1', $item['name'], "Item was retrieved unsuccessfully"); + } +} +``` + +The test above should be able to test SOAP and REST, depending on what adapter is currently used by the testing framework. The format of `$serviceInfo` is defined by the Web API client adapter interface: + +```php +namespace Magento\TestFramework\TestCase\Webapi; + +interface AdapterInterface +{ + /** + * Perform call to the specified service method. + * + * @param array $serviceInfo
    +     * array(
    +     *     'rest' => array(
    +     *         'resourcePath' => $resourcePath, // e.g. /products/:id
    +     *         'httpMethod' => $httpMethod,     // e.g. GET
    +     *         'token' => '21hasbtlaqy8t3mj73kjh71cxxkqj4aq'    // optional : for token based Authentication. Will
    +     *                                                             override default OAuth based authentication provided
    +     *                                                             by test framework
    +     *     ),
    +     *     'soap' => array(
    +     *         'service' => $soapService,    // soap service name with Version suffix e.g. catalogProductV1, customerV2
    +     *         'operation' => $operation     // soap operation name e.g. catalogProductCreate
    +     *     )
    +     * );
    +     * 
    + * @param array $arguments + * @param string|null $storeCode if store code not provided, default store code will be used + * @param \Magento\Integration\Model\Integration|null $integration + * @return array|string|int|float|bool + */ + public function call($serviceInfo, $arguments = [], $storeCode = null, $integration = null); +} +``` + +## How to Run the Tests {#howto} + +### Prerequisites {#prereq} + +1. Install the [PHP](https://glossary.magento.com/php) Soap [extension](https://glossary.magento.com/extension). + + Copy `php_soap.dll` or `php_soap.so` to your PHP extensions directory. Edit your `php.ini` file and enable the PHP Soap extension. Usually this means deleting the leading semi-colon in front of the extension. Then restart Apache. + + `extension=php_soap.dll` + +1. Before running the functional tests you need to clear your [cache](https://glossary.magento.com/cache). Now you are ready to run the tests. + +### Running the Tests {#running} + +1. Copy `dev/tests/api-functional/phpunit_rest.xml.dist` and `phpunit_soap.xml.dist` to `dev/tests/api-functional/phpunit_rest.xml` and `phpunit_soap.xml`. + +1. Define the Magento instance URL as a value of `TESTS_BASE_URL`, Test Webservice User as value of `TESTS_WEBSERVICE_USER` and Test Webservice API key as value of `TESTS_WEBSERVICE_APIKEY` in copied file i.e. `phpunit_rest.xml` or `phpunit_soap.xml`. + +1. Copy `dev/tests/api-functional/config/install-config-mysql.php.dist` to `dev/tests/api-functional/config/install-config-mysql.php`. + +1. Configure your DB connection and install settings in `dev/tests/api-functional/config/install-config-mysql.php`. Specify the Magento database. The base URL to access this Magento instance must be the same specified in the `phpunit_rest.xml` or `phpunit_soap.xml` file. + +1. Run `phpunit` using the `dev/tests/api-functional/phpunit_rest.xml` or `dev/tests/api-functional/phpunit_soap.xml` configuration file:: + + ```bash + vendor/bin/phpunit --configuration + ``` + + or + + ```bash + vendor/bin/phpunit -c + ``` diff --git a/src/guides/v2.3/graphql/authorization-tokens.md b/src/guides/v2.3/graphql/authorization-tokens.md new file mode 100644 index 00000000000..9b39a362165 --- /dev/null +++ b/src/guides/v2.3/graphql/authorization-tokens.md @@ -0,0 +1,54 @@ +--- +group: graphql +title: Authorization tokens +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +redirect_from: + - /guides/v2.3/graphql/get-customer-authorization-token.html +--- + +Magento provides separate token services for customers and administrators. When you request a token from one of these services, the service returns a unique access token in exchange for the username and password for a Magento account. + +Magento GraphQL provides a mutation that returns a token on behalf of a logged-in customer. You must use a REST call to fetch an admin token. Use this token in the Authorization request header field for any queries and mutations. See [Request headers]({{page.baseurl}}/graphql/send-request.html#headers) + +## Customer tokens + +The [`generateCustomerToken` mutation]({{page.baseurl}}/graphql/mutations/generate-customer-token.html) requires the customer email address and password in the payload, as shown in the following example: + +**Request:** + +```text +mutation { + generateCustomerToken(email: "customer@example.com", password: "password") { + token + } +} +``` + +**Response:** + +```json + { + "data": { + "generateCustomerToken": { + "token": "hoyz7k697ubv5hcpq92yrtx39i7x10um" + } + } + } +``` + +You can now use this token in the Authorization request header field for any queries and mutations. + +![GraphQL Authorization Bearer]({{site.baseurl}}/common/images/graphql/graphql-authorization.png) + +If necessary, you also can [revoke the customer's token]({{page.baseurl}}/graphql/mutations/revoke-customer-token.html) + +By default, a customer token is valid for 1 hour. You can change these values from Admin by selecting **Stores** > **Settings** > **Configuration** > **Services** > **OAuth** > **Access Token Expiration** > **Customer Token Lifetime**. + +## Admin tokens + +In Magento GraphQL, you specify an admin token only if you need to query products, categories, price rules, or other entities that are scheduled to be in a campaign (staged content). Staging is supported in {{site.data.var.ee}} only. See [Staging queries]({{page.baseurl}}/graphql/queries/index.html#staging) for more information. + +Magento does not provide a GraphQL mutation that generates an admin token. You must use the `POST /V1/integration/admin/token` REST endpoint instead. [Generate the admin token]({{page.baseurl}}/rest/tutorials/prerequisite-tasks/create-admin-token.html) shows how to use this endpoint. + +By default, an admin token is valid for 4 hours. You can change these values from Admin by selecting **Stores** > **Settings** > **Configuration** > **Services** > **OAuth** > **Access Token Expiration** > **Admin Token Lifetime**. diff --git a/src/guides/v2.3/graphql/caching.md b/src/guides/v2.3/graphql/caching.md new file mode 100644 index 00000000000..4470c659082 --- /dev/null +++ b/src/guides/v2.3/graphql/caching.md @@ -0,0 +1,150 @@ +--- +group: graphql +title: GraphQL caching +--- + +Magento can cache pages rendered from the results of certain GraphQL queries with [full-page caching]({{page.baseurl}}/extension-dev-guide/cache/page-caching.html). Full-page caching improves response time and reduces the load on the server. Without caching, each page might need to run blocks of code and retrieve large amounts of information from the database. Only queries submitted with an HTTP GET operation can be cached. POST queries cannot be cached. + +## Cached and uncached queries + +The definitions for some queries include cache tags. Full page caching uses these tags to keep track of cached content. They also allow public content to be invalidated. Private content invalidation is handled on the client side. + +{:.bs-callout-info} +GraphQL allows you to make multiple queries in a single call. If you specify any query that Magento does not cache, Magento bypasses the cache for all queries in the call. + +Magento caches the following queries: + +* `category` (deprecated) +* `categoryList` +* `cmsBlocks` +* `cmsPage` +* `products` +* `urlResolver` + +Magento explicitly disallows caching the following queries. + +* `cart` +* `country` +* `countries` +* `currency` +* `customAttributeMetadata` +* `customer` +* `customerDownloadableProducts` +* `customerOrders` +* `customerPaymentTokens` +* `storeConfig` +* `wishlist` + +[Define the GraphQL schema for a module]({{page.baseurl}}/graphql/develop/create-graphqls-file.html) describes the syntax of a valid query. + +## Caching with Varnish + +We recommend setting up Varnish as a reverse proxy to serve the full page cache in a production environment. See [Configure and use Varnish]({{page.baseurl}}/config-guide/varnish/config-varnish.html) for more information. + +As of Magento 2.3.2, Magento supports GraphQL caching with Varnish. If you have upgraded from a previous version, you can enable GraphQL caching by generating a new template file, or by editing the `default.vcl` file on your system to match the current default template for your version of Varnish. + +If you choose to edit an existing `default.vcl` file, update the `vcl_hash` subroutine to check whether the request URL contains `graphql`, as follows: + +```text +sub vcl_hash { + if (req.http.cookie ~ "X-Magento-Vary=") { + hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1")); + } + + # For multi site configurations to not cache each other's content + if (req.http.host) { + hash_data(req.http.host); + } else { + hash_data(server.ip); + } + + if (req.url ~ "/graphql") { + call process_graphql_headers; + } + + # To make sure http users don't see ssl warning + if (req.http./* {{ ssl_offloaded_header }} */) { + hash_data(req.http./* {{ ssl_offloaded_header }} */); + } +} +``` + +Then add the `process_graphql_headers` subroutine: + +```text +sub process_graphql_headers { + if (req.http.Store) { + hash_data(req.http.Store); + } + if (req.http.Content-Currency) { + hash_data(req.http.Content-Currency); + } +} +``` + +Query results should not be cached for logged in customers, because it cannot be guaranteed that these results are applicable to all customers. For example, you can create multiple customer groups and set up different product prices for each group. Caching results like these might cause customers to see the prices of another customer group. + +To prevent customers from seeing the incorrect data from cached results, add the following to your `.vcl` file in the `vcl_recv` subroutine before the return (hash): + +```text +# Authenticated GraphQL requests should not be cached by default +if (req.url ~ "/graphql" && req.http.Authorization ~ "^Bearer") { + return (pass); +} +``` + +This statement prevents any query with an authorization token from being cached. + +[Configure Varnish and your web server]({{page.baseurl}}/config-guide/varnish/config-varnish-configure.html) further describes how to configure the `default.vcl` file. + +## Caching with Fastly + +To cache GraphQL query results on {{ site.data.var.ece }}, the Cloud project must be running Fastly CDN module for Magento 2 version 1.2.118 or later. + +{:.procedure} +To enable GraphQL caching on Fastly: + +1. Upgrade the Fastly CDN Module for Magento 2.x to version 1.2.118 or later. +1. Upload the updated VCL code to the Fastly servers. + +[Set up Fastly]({{ site.baseurl }}/cloud/cdn/configure-fastly.html) describes how to perform both of these tasks. + +By default, the Fastly module for Magento provides the following VCL configuration for GraphQL caching: + +```text +if (req.request == "GET" && req.url.path ~ "/graphql" && req.url.qs ~ "query=") { +.... +``` + +Fastly will only cache GET requests that contain a query parameter in the request URL. + +### Example + +```text +http://example.com/graphql?query={ products(filter: {sku: {eq: "Test"}}) { items { name } } }&variables={} +.... +``` + +{:.bs-callout-info} +If you call GraphQL queries in the query body rather than the URL (for example, as `--data-raw '{"query" .... }'`), the request is not cached. + +## X-Magento-Vary + +The `X-Magento-Vary` cache cookie is not supported for GraphQL. The `Store` and `Content-Currency` headers, along with the content language (which is deduced) determine the context. + +## Response headers + +In developer mode, Magento returns several headers that could be useful for debugging caching problems. These headers are not specific to GraphQL. + +Header | Description +--- |--- +`X-Magento-Cache-Debug` | HIT (the page was loaded from cache) or MISS (the page was not loaded from cache. +`X-Magento-Tags` | A list of cache tags that correspond to the catalog, category, or CMS items returned in the query. Magento caches these items. + +## Cache invalidation + +Magento invalidates the cache when any of the following events occur: + +* When a change occurs to a specific entity or entities in aggregate. An increase in a product's price is a direct and obvious change. Applying a new tax class tax to products changes a set of products in aggregate. +* When system configuration changes +* When an administrator flushes or disables the cache from the Admin or with the `bin/magento cache` command diff --git a/src/guides/v2.3/graphql/custom-filters.md b/src/guides/v2.3/graphql/custom-filters.md new file mode 100644 index 00000000000..a5e9c0efeb9 --- /dev/null +++ b/src/guides/v2.3/graphql/custom-filters.md @@ -0,0 +1,144 @@ +--- +group: graphql +title: Filtering with custom attributes +--- + +As of Magento 2.3.4, the `filter` attribute of the [`products`]({{page.baseurl}}/graphql/queries/products.html) query accepts the `ProductAttributeFilterInput` object. (In previous versions, the `filter` attribute required a `ProductFilterInput` object. This object contained a hard-coded list of filterable attributes, and you could not filter on a custom attribute or any other attribute that was not on the list.) + +## Prerequisites + +You have several options when enabling a custom attribute (or any attribute that is not listed by default in the `ProductAttributeFilterInput` object) for filtering. Navigate to the attribute's **Storefront Properties** page (**Stores** > Attributes > **Product** > <attribute name> > **Storefront Properties**) in the Admin, then perform one or both of the following actions: + +- Set the **Use in Layered Navigation** field to **Filterable (with results)** or **Filterable (no results)**. This field allows the attribute to be used as a filter and returns layered navigation and aggregation data. If this field is set to **No**, then the attribute will not return layered navigation and aggregation data. + +- Set the **Use in Search** and **Visible in Advanced Search** fields to **Yes**. These fields primarily allow Magento to index the attribute's contents, making the data available for quick and advanced searches. Setting both these fields also allows the attribute to be used as a filter. These fields do not configure the presence or absence of layered navigation and aggregation data. If you set only one of these fields to **Yes**, the attribute cannot be used as a filter (unless you set the **Use in Layered Navigation** field to a value other than **No**). + +## Define the filter for your query + +The [`filter`]({{page.baseurl}}/graphql/queries/products.html#ProductFilterInput) definition for your custom attribute requires one of the following input data types: + +- `FilterEqualTypeInput` - Specify this data type when the **Catalog Input Type for Store Owner** field for your custom attribute is set to Yes/No, Select, or Multiple select. Your filter can contain the `eq` or `in` attribute. Use the `eq` attribute to exactly match the specified string. Use the `in` attribute to filter on a comma-separated list of values. +- `FilterMatchTypeInput` - Specify this data type when the **Catalog Input Type for Store Owner** field for your custom attribute is set to Text Field or Text Area. Your filter must contain the `match` attribute, which will return all items that partially fuzzy match the specified string. +- `FilterRangeTypeInput` - Specify this data type when the **Catalog Input Type for Store Owner** field for your custom attribute is set to Price or Date. Your filter can contain one or both of the `to` and `from` attributes, which serve to provide a range of values to filter on. + +## Example + +In this example, the custom attribute `volume` was assigned to the `bags` attribute group. Running the [`customAttributeMetadata` query]({{page.baseurl}}/graphql/queries/custom-attribute-metadata.html) on this custom attribute reveals that the `label` and `value` values for the attribute's options are as follows: + +**Request:** + +```graphql +{ + customAttributeMetadata( + attributes: [ + { + attribute_code: "volume" + entity_type: "catalog_product" + } + ] + ) { + items { + attribute_code + attribute_type + entity_type + input_type + attribute_options { + value + label + } + } + } +} +``` + +**Response:** + +```graphql +{ + "data": { + "customAttributeMetadata": { + "items": [ + { + "attribute_code": "volume", + "attribute_type": "Int", + "entity_type": "catalog_product", + "input_type": "select", + "attribute_options": [ + { + "value": "216", + "label": "Large" + }, + { + "value": "217", + "label": "Medium" + }, + { + "value": "218", + "label": "Small" + } + ] + } + ] + } + } +} +``` + +`label` | `value` +--- | --- +`Large` | `216` +`Medium` | `217` +`Small` | `218` + +In this scenario, a [`products`]({{page.baseurl}}/graphql/queries/products.html) search filtered to return items where the `volume` attribute is set to `Large` would be similar to the following: + +**Request:** + +```graphql +{ + products(filter: { volume: { eq: "216" } }) { + total_count + items { + name + sku + } + } +} +``` + +**Response:** + +The response might be similar to the following: + +```json +{ + "data": { + "products": { + "total_count": 1, + "items": [ + { + "name": "Wayfarer Messenger Bag", + "sku": "24-MB05" + } + ] + } + } +} +``` + +## Output attributes + +When a product requires a filter attribute that is not a field on its output schema, inject the attribute name into the class in a module's `di.xml` file. + +```xml + + + + field + other_field + + + +``` + +This example adds `field_to_sort` and `other_field_to_sort` attributes to the `additionalAttributes` array defined in the `ProductEntityAttributesForAst` class. The array already contains the `min_price`, `max_price`, and `category_ids` attributes. diff --git a/src/guides/v2.3/graphql/develop/create-custom-url-resolver.md b/src/guides/v2.3/graphql/develop/create-custom-url-resolver.md new file mode 100644 index 00000000000..a0a729415ff --- /dev/null +++ b/src/guides/v2.3/graphql/develop/create-custom-url-resolver.md @@ -0,0 +1,64 @@ +--- +group: graphql +title: Create a custom GraphQL urlResolver service +--- + +The `Magento\UrlRewrite` module converts URL rewrite requests to canonical URLs. As a result, your custom `urlResolver` module does not require its own class for performing these actions, but it must be able to save and delete entries in the `url_rewrite` table. + +## Create observers + +You can use the `Magento\CmsUrlRewrite\Observer\ProcessUrlRewriteSavingObserver` class as the basis for saving URL rewrites. For deleting entries, create a `ProcessUrlRewriteDeleteObserver` class similar to the following: + +```php +/** + * Generate urls for UrlRewrite and save it in storage + * + * @param \Magento\Framework\Event\Observer $observer + * @return void + */ +public function execute(EventObserver $observer) +{ + /** @var \Magento\MyModule\Model\Page $myEntityPage */ + $page = $observer->getEvent()->getObject(); + + if ($page->isDeleted()) { + $this->urlPersist->deleteByData( + [ + UrlRewrite::ENTITY_ID => $page->getId(), + UrlRewrite::ENTITY_TYPE => MyEntityPageUrlRewriteGenerator::ENTITY_TYPE, + ] + ); + } +} +``` +See [Events and observers]({{ page.baseurl }}/extension-dev-guide/events-and-observers.html) for more information about creating an observer. + +## Configure the custom module + +Update the `graphql.xml` and `events.xml` file in your module's `etc` directory to configure your custom GraphQL `urlResolver` service: + +* Add lines similar to the following in your module's `graphql.xml` file to define the enumeration. The `UrlRewriteGraphQl` module defines `UrlRewriteEntityTypeEnum`. + + ```xml + + + MY_ENTITY + + + ``` + +* Define two events similar to the following in your module's `events.xml` file. + + ```xml + + + + + + + ``` + +## Related Topics + +* [Events and observers]({{ page.baseurl }}/extension-dev-guide/events-and-observers.html) +* [urlResolver endpoint]({{ page.baseurl }}/graphql/queries/url-resolver.html) diff --git a/src/guides/v2.3/graphql/develop/create-graphqls-file.md b/src/guides/v2.3/graphql/develop/create-graphqls-file.md new file mode 100644 index 00000000000..0a5b57c43c8 --- /dev/null +++ b/src/guides/v2.3/graphql/develop/create-graphqls-file.md @@ -0,0 +1,265 @@ +--- +group: graphql +title: Define the GraphQL schema for a module +redirect_from: /guides/v2.3/graphql/develop/configure-graphql-xml.html +--- + +Each module that adds to or extends from a GraphQL schema can do so by placing a `schema.graphqls` file in its `etc` directory. Magento Core adds [`GraphQl`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/GraphQl) modules based on the purpose of the schema being extended/added and the core modules they depend on. For example, the `CustomerGraphQl` module adds a query and multiple mutations to the `graphql` endpoint to view and modify customer data. The `CustomerGraphQl` module relies on the `Customer` core module. + +A GraphQL module's `schema.graphqls` file defines how the attributes defined in the module can be used in GraphQL queries and mutations. If your module's attributes are completely self-contained, then the `schema.graphqls` file defines the queries, mutations, the interfaces used, the data types of all the attributes, and any enumerations that restrict the possible attribute contents. If your module extends another module, then you must define those attributes and ensure that the other module can load your attributes. For example, the `CatalogGraphQl` module defines the `PriceAdjustmentCodesEnum`, but the `TaxGraphQl` and `WeeeGraphQl` modules define the enumeration values. + +The `/etc/schema.graphqls` file: + +* Defines the structure of queries and mutations. +* Determines which attributes can be used for input and output in GraphQL queries and mutations. Requests and responses contain separate lists of valid attributes. +* Points to the resolvers that verify and process the input data and response. +* Serves as the source for displaying the schema in a GraphQL browser. +* Defines which objects are cached. + +The base `schema.graphqls` file, located in the `app/code/Magento/GraphQl/etc/` directory, defines the basic structure of GraphQL queries and mutations. It also includes definitions for comparison operators and paging information for search results. The `webonyx/graphql-php` library enforces the syntax of all `schema.graphqls` files. + +## Define queries + +A query definition can be one line, or it can be complex. If your module's query implements `searchCriteria`, then you must define arguments that define filters and pagination information, all of which adds complexity. However, if you expect a single result from your query, then its definition can be simple. + +The following example shows the `products` query. The `type` is defined as a `Query`. The `products` definitions define the keywords that are used to construct a query, as shown in [Using queries]({{ page.baseurl }}/graphql/queries/index.html). The parameter definitions will be discussed in [Specify output attributes](#specify-output-attributes). + +```text +type Query { + products ( + search: String + filter: ProductAttributeFilterInput + pageSize: Int = 20 + currentPage: Int = 1 + sort: ProductAttributeSortInput + ): Products @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Products") +} +``` + +In contrast, the `customer` query returns the `Customer` object associated with the current user. There is no need to define pagination information. + +```text +type Query { + customer: Customer @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\Customer") +} +``` + +If all your module's attributes are extension attributes for existing modules, then no query definition is required. In this case, the attributes point to the other module's query definition. + +### Declare input attributes + +You must explicitly define each attribute that can be used as input in a GraphQL query. In the simplest cases, you can create a single `type` definition that includes all the input, output, and sorting attributes for an object. This might not be possible if your module performs calculations, or otherwise has attributes that aren't available at the time of the query. + +The following example shows the `products` query, which has multiple optional attributes: + +```graphql +products( + search: String + filter: ProductAttributeFilterInput + pageSize: Int + currentPage: Int + sort: ProductAttributeSortInput +): Products +``` + +The `ProductAttributeFilterInput` object used in the `filter` attribute is a custom input type that determines which attributes can be used to narrow the results in a `products` query. The attributes of this object are of type `FilterEqualTypeInput`. (These entities are defined in the `etc/schema.graphqls` files of the `GraphQl` and `CatalogGraphQl` modules). In other use cases, you would be required to create your own input type in the `/app/code///etc/schema.graphqls` file. + +The following attributes can be used as filters using the `ProductAttributeFilterInput` object. + +```graphql +input ProductAttributeFilterInput { + category_id: FilterEqualTypeInput +} +``` + +The `FilterEqualTypeInput` type defines a filter that matches the input exactly. + +```graphql +input FilterEqualTypeInput { + in: [String] + eq: String +} +``` + +The following example filter searches for products whose `category_id` equals 1. + +```graphql +{ + products(filter: {category_id: {eq: "1"}}) { + total_count + items { + name + } + } +} +``` + +The search returns products whose `category_id` equals 1. + +```json +{ + "data": { + "products": { + "total_count": 2, + "items": [ + { + "name": "Josie Yoga Jacket" + }, + { + "name": "Selene Yoga Hoodie" + } + ] + } + } +} +``` + +### Specify output attributes {#specify-output-attributes} + +You must know the data type of each attribute, whether it is scalar or an object, and whether it can be part of an array. In addition, each attribute within an object must be defined in the same manner. + +In a `schema.graphqls` file, the output `Interface` defines top-level attributes. Each object returned is defined in a `type` definition. + +The following example shows the `products` query. The query returns a `Products` object containing the attributes of the specified data types. + +Attribute | Data type | Description +--- | --- | --- +`aggregations` | [[Aggregation]]({{ page.baseurl }}/graphql/queries/products.html#Aggregation) | Layered navigation aggregations +`items` | [[ProductInterface]]({{ page.baseurl }}/graphql/queries/products.html#ProductInterface) | An array of products that match the specified search criteria +`page_info` | [SearchResultPageInfo]({{ page.baseurl }}/graphql/queries/products.html#SearchResultPageInfo) | An object that includes the `page_info` and `currentPage` values specified in the query +`sort_fields` | [SortFields]({{ page.baseurl }}/graphql/queries/products.html#SortFields) | An object that includes the default sort field and all available sort fields +`total_count` | Int | The number of products in the category that are marked as visible. By default, in complex products, parent products are visible, but their child products are not + +### Define the output interface + +In many cases, the response contains data that was either not available as input, or was transformed in some manner from the input. For example, when you specify a price in an input filter, Magento evaluates it as a Float value. However, `Price` output objects contain a Float value, a currency value, and possibly minimum/maximum values and tax adjustments. You can define a `typeResolver` to point to the Resolver object, which interprets the GraphQL query. If your module contains only attributes that extend another module, then this parameter is optional. Otherwise, it is required. See [Resolvers]({{ page.baseurl }}/graphql/develop/resolvers.html) for more information. + +Output types that represent entities that can be manipulated (created, updated, or removed) and/or can be cached on the client MUST have `id` field. The type of the field SHOULD be `ID`. + +The following example shows the `products` query. The `page_info` attribute contains the `SearchResultPageInfo` data type which is defined in the `schema.graphqls` file under `ModuleGraphQl`. In other use cases, you would be required to create your own output type in the `/app/code///etc/schema.graphqls` file. + +The SearchResultPageInfo provides navigation for the query response. + +```graphql +type SearchResultPageInfo { + page_size: Int + current_page: Int + total_pages: Int +} +``` + +The following example uses the `page_info` output attribute which is of `SearchResultPageInfo` type to get all the information related to the page. + +```graphql +{ + products(search: "Yoga pants", pageSize: 2) { + total_count + items { + name + } + page_info { + page_size + current_page + } + } +} +``` + +The search returns 45 items, but only the first two items are returned on the current page and all the information regarding the page is returned. + +```json +{ + "data": { + "products": { + "total_count": 45, + "items": [ + { + "name": "Josie Yoga Jacket" + }, + { + "name": "Selene Yoga Hoodie" + } + ], + "page_info": { + "page_size": 2, + "current_page": 1 + } + } + } +} +``` + +## Define mutations + +A mutation definition contains the following information: + +* The mutation name +* The input attributes and objects +* The attributes and objects that can be returned in the output +* The path to the resolver + +The following mutation creates a customer. + +``` text +type Mutation { + createCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") +} +``` + +This mutation requires the `CustomerInput` object, which defines the customers name, email, password, and other attributes, as input. + +Input parameters can be optional when the context is provided in a header or other mechanism. + +## Define enumerations + +You can optionally define enumerations to help prevent input errors. Magento capitalizes all enumerated responses. If a value contains a dash (-), the system converts it to an underscore (_). This is done to maintain compliance with the GraphQL specification. + +## Annotations + +You can describe any attribute, type definition, or other entity within a `schema.graphqls` file by appending the following to the line: + +`@doc(description: "")` + +For example: + +```text +sku: FilterTypeInput @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer") +url_key: String @doc(description: "The url key assigned to the product") +product_count: Int @doc(description: "The number of products") +``` + +Use the `@deprecated` directive to deprecate attributes and enum values. The GraphQL specification does not permit deprecating input values or arguments. The `reason` keyword allows you to specify which attribute/field or enum value should be used instead. + +For example: + +```text +type Query { + cmsPage ( + id: Int @doc(description: "Id of the CMS page") @deprecated(reason: "Use `identifier`") @doc(description: "The CMS page ...") + identifier: String @doc(description: "Identifier of the CMS page") +... +``` + +## Query caching + +The `@cache` directive defines whether the results of certain queries can be cached. Queries relating to products, categories, and CMS may be cached. + +Define cachable queries in the following manner: + +```text +@cache(cacheIdentity: "Magento\\CmsGraphQl\\Model\\Resolver\\Block\\Identity") +``` + +The `cacheIdentity` value points to the [class]({{page.baseurl}}/graphql/develop/identity-class.html) responsible for retrieving cache tags. + +A query without a `cacheIdentity` will not be cached. + +To disable caching for queries declared in another module with a `cacheIdentity` class, the `@cache(cacheable: false)` directive can be used. +This `cacheable` argument is intended to disable caching for queries that are defined in another module. + +`@cache(cacheable: false)` + +Specifying `@cache(cacheable: false)` or `@cache(cacheable: true)` on a query without a `cacheIdentity` class has no effect: the query will not be cached. +If a query should **not** be cached, do not specify the `@cache` directive. Specifying `@cache(cacheable: false)` is superfluous when no `cacheIdentity` is present. + +See [Create a cache type]({{page.baseurl}}/extension-dev-guide/cache/partial-caching/create-cache-type.html) for information about enabling caching for custom modules. diff --git a/src/guides/v2.3/graphql/develop/debugging.md b/src/guides/v2.3/graphql/develop/debugging.md new file mode 100644 index 00000000000..c9808d965f9 --- /dev/null +++ b/src/guides/v2.3/graphql/develop/debugging.md @@ -0,0 +1,32 @@ +--- +group: graphql +title: Debugging GraphQL queries +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +This topic provides recommendations on how to debug GraphQL requests. + +## Debugging with PHPStorm and Xdebug + +When [using GraphiQL]({{ page.baseurl }}/graphql/index.html#how-to-access-graphql) or any other client for testing GraphQL queries, you might need to debug the request processing. +You can use Xdebug for debugging the PHP execution of a GraphQL query just as you would for other HTTP requests. +To start debugging, add the `?XDEBUG_SESSION_START=PHPSTORM` parameter to the endpoint URL. +The following example shows how to establish a connection between Xdebug and PHPStorm IDE. + +```http +http:///graphql?XDEBUG_SESSION_START=PHPSTORM +``` + +You can also enable an Xdebug connection for a particular request by setting the corresponding header parameter. + +```text +Cookie: XDEBUG_SESSION=PHPSTORM +``` + +As a result, Xdebug within the PHP execution attempts to make a connection to an IDE. If the IDE is listening, it will give the instructions to Xdebug about breakpoints and other relevant information. + +## Related Topics + +* [GraphQL request headers]({{ page.baseurl }}/graphql/send-request.html) +* [Exception handling]({{ page.baseurl }}/graphql/develop/exceptions.html) diff --git a/src/guides/v2.3/graphql/develop/exceptions.md b/src/guides/v2.3/graphql/develop/exceptions.md new file mode 100644 index 00000000000..ea6c8e53f1b --- /dev/null +++ b/src/guides/v2.3/graphql/develop/exceptions.md @@ -0,0 +1,18 @@ +--- +group: graphql +title: Exception handling +--- + +The WebApi module has an implementation to “mask” `LocalizedExceptions` so they aren't exposed to the client. GraphQL accomplishes this by restricting verbose output to only those exceptions implementing `\GraphQL\Error\ClientAware`, and only if the system is in developer mode. In these circumstances, Magento returns a full stack trace. Otherwise, Magento writes these exceptions to the system `exception.log` file while returning an “internal server error” to the client. + +You should implement the `\GraphQL\Error\ClientAware` interface to handle errors in your module that are directly related to a GraphQL field having an anticipated exception. If you don't, the client will not receive useful messages. However, you should ensure that you don't implement the `ClientAware` interface for too many exceptions. Doing this risks exposing sensitive data to the client. + +Magento provides the following exception classes in `Magento\Framework\GraphQl\Exception`. + +Class | Exception category | Description +--- | --- | --- +`GraphQlAlreadyExistsException` | `graphql-already-exists` | Thrown when data already exists +`GraphQlAuthenticationException` | `graphql-authentication` | Thrown when an authentication fails +`GraphQlAuthorizationException` | `graphql-authorization` | Thrown when an authorization error occurs +`GraphQlInputException` | `graphql-input` | Thrown when a query contains invalid input +`GraphQlNoSuchEntityException` | `graphql-no-such-entity` | Thrown when an expected resource doesn't exist diff --git a/src/guides/v2.3/graphql/develop/extend-existing-schema.md b/src/guides/v2.3/graphql/develop/extend-existing-schema.md new file mode 100644 index 00000000000..b6ef7983c8c --- /dev/null +++ b/src/guides/v2.3/graphql/develop/extend-existing-schema.md @@ -0,0 +1,135 @@ +--- +group: graphql +title: Extend an existing GraphQL schema +contributor_name: Adarsh Manickam +contributor_link: https://github.com/drpayyne +--- + +You can extend the default Magento GraphQL schema to add attributes and data types, modify existing resolver behavior, and add features using other extension points. GraphQL uses _stitching_ to assemble a single unified schema out of the many schemas defined in individual modules. All `schema.graphqls` files are stitched together to a single schema. In this process, all nodes with the same type (such as type, interface, and enum) and name are stitched together and recursively extended/overridden. This process is similar to how XML merging works. + +## Extend the schema + +The first step to retrieve a custom field in an existing query is to extend the appropriate schema object. + +In the following example, we will change the description of an existing field (`attribute_set_id`) and add a new field (`attribute_set_name`) to the GraphQL schema for the `products` query. Common use cases require adding fields to the database. [Declarative Schema]({{ page.baseurl }}/extension-dev-guide/declarative-schema/) describes how to add a custom field to the database. + +The simplified structure of the query schema to get products is: + +```graphql +schema { + query: Query + ... +} + +type Query { + products (...): Products + ... +} + +type Products { + items: [ProductInterface] + ... +} + +interface ProductInterface { + id: Int + name: String + sku: String + ... +} +``` + +We need to extend the `ProductInterface`, since that is the schema object for a product. We can do this by creating a `schema.graphqls` file in our custom module's (`ExampleCorp/CustomGQL`) `etc` directory. + +`ExampleCorp_CustomGQL/etc/schema.graphqls` + +```graphql +interface ProductInterface { + attribute_set_id: Int + @doc(description: "ID of the attribute set assigned to the product") + attribute_set_name: String + @doc(description: "Name of attribute set assigned to the product") + @resolver(class: "\\ExampleCorp\\CustomGQL\\Model\\Resolver\\ProductAttributeSetNameResolver") +} +``` + +The above schema file is merged with the schema present at `Magento_CatalogGraphQl/etc/schema.graphqls` which contains the original `ProductInterface` object. Our schema file contains the following fields: + +- The `attribute_set_id` field is already present in the original schema, so the field described in our new schema will override the field present in the `ProductInterface` object. This example only changes the `@doc` annotation content to demonstrate how the process works. + +- The `attribute_set_name` field is not present in the orignal schema, so the field is added to the `ProductInterface` object by extending it. For our new field, we set a description and a resolver class to resolve the data to be returned. + +## Resolve the field value + +In the resolver, we get the relevant data based on the `$value` and `$args` passed to the `resolve` method. This can be done using a repository interface or a resource model of the custom field. + +In our example scenario, we use `Magento\Catalog\Api\AttributeSetRepositoryInterface` to get the attribute set name for a given attribute set ID obtained from the `$value` argument and return that as the resolution for the field. + +`ExampleCorp_CustomGQL/Model/Resolver/ProductAttributeSetNameResolver.php` + +```php +setRepository = $setRepository; + } + + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + return $this->setRepository->get($value['attribute_set_id'])->getAttributeSetName(); + } +} +``` + +## Extend configuration data + +You can add your own configuration to the `storeConfig` query within your own module. + +To do this, configure the constructor argument `extendedConfigData` in the `argument` node in your area-specific `etc/graphql/di.xml` file. + +The following example adds an array-item to the `extendedConfigData` array within the construct of the `StoreConfigDataProvider`. + +```xml + + + + + + section/group/field + + + + +``` + +You must also extend the type `storeConfig` within in the `etc/schema.graphqls` file, as shown below: + +```graphql +type StoreConfig { + section_group_field : String @doc(description: "Extended Config Data - section/group/field") +} +``` + +## Related topics + +- [Define the GraphQL schema for a module]({{ page.baseurl }}/graphql/develop/create-graphqls-file.html) +- [Resolvers]({{ page.baseurl }}/graphql/develop/resolvers.html) +- [Declarative schema]({{ page.baseurl }}/extension-dev-guide/declarative-schema/) diff --git a/src/guides/v2.3/graphql/develop/identity-class.md b/src/guides/v2.3/graphql/develop/identity-class.md new file mode 100644 index 00000000000..bfbd31329d1 --- /dev/null +++ b/src/guides/v2.3/graphql/develop/identity-class.md @@ -0,0 +1,59 @@ +--- +group: graphql +title: Identity class +--- + +If you create a cacheable query (similar to those for product, category, and CMS data), then you must create an `Identity` class for the module. The class must return unique identifiers for cache tags that can be invalidated when an entity changes. Place this class in your module’s `Model/Resolver` directory. + +An Identity class implements `Magento\Framework\GraphQl\Query\Resolver\IdentityInterface`. Your Identity class must contain the following elements: + +* Choose a cache tag prefix for the entity. + +* Your implementation of the `getIdentities(array $resolvedData)` method. The method maps the array of entities data to an array of cache tags, one for each entity. Generally, this method takes an array of query results and creates a cache tag for each entity based on the original string and the unique identifier for each item to be cached. For example, the `getIdentities` method for the `CatalogGraphQl` component appends the product ID to the `cat_p` cache tag prefix, such as `cat_p_1`, `cat_p_2`, and so on. Usually the method also adds the cache tag without an appended ID to the result array, so all cache records can be removed at once, and not only cache records for specific entities. + +Use following example as the basis for your custom `Identity` class: + +```php +cacheTag, $item['entity_id']); + } + if (!empty($ids)) { + $ids[] = $this->cacheTag; + } + return $ids; + } +} +``` + +Use the `@cache` directive in your module’s [`graphqls` file]({{page.baseurl}}/graphql/develop/create-graphqls-file.html) to specify the location to your `Identity` class. Your module’s `graphqls` file must point to your `Identity` class, as shown below: + +```text + categoryList( + filters: CategoryFilterInput @doc(description: "Identifies which Category filter inputs to search for and return.") + ): [CategoryTree] @doc(description: "Returns an array of categories based on the specified filters.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\CategoryList") @cache(cacheIdentity: "Magento\\CatalogGraphQl\\Model\\Resolver\\Category\\CategoriesIdentity") +} +``` diff --git a/src/guides/v2.3/graphql/develop/resolvers.md b/src/guides/v2.3/graphql/develop/resolvers.md new file mode 100644 index 00000000000..fc0db34133c --- /dev/null +++ b/src/guides/v2.3/graphql/develop/resolvers.md @@ -0,0 +1,367 @@ +--- +group: graphql +title: Resolvers +--- + +A resolver performs GraphQL request processing. In general, it is responsible for constructing a query, fetching data and performing any calculations, then transforming the fetched and calculated data into a GraphQL array format. Finally, it returns the results wrapped by a callable function. + +A GraphQL request is represented by the following arguments, which will be processed by a resolver: + +Field | Type | Description +--- | --- | --- +$field | [`Magento\Framework\GraphQl\Config\Element\Field`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Config/Element/Field.php) | Fields are used to describe possible values for a type/interface +$context | [`Magento\Framework\GraphQl\Query\Resolver\ContextInterface`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Query/Resolver/ContextInterface.php) | Resolver context is used as a shared data extensible object in all resolvers that implement [`ResolverInterface`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Query/ResolverInterface.php). +$info | [`Magento\Framework\GraphQl\Schema\Type\ResolveInfo`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Schema/Type/ResolveInfo.php) | Structure containing information useful for field resolution process. +$value | array | Contains additional query parameters. `Null` in most cases. +$args | array | Contains input arguments of query. + +A GraphQL resolver must implement one of the following interfaces: + +- [`\Magento\Framework\GraphQl\Query\Resolver\BatchResolverInterface`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Query/Resolver/BatchResolverInterface.php) + +- [`\Magento\Framework\GraphQl\Query\Resolver\BatchServiceContractResolverInterface`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Query/Resolver/BatchServiceContractResolverInterface.php) + +- [`\Magento\Framework\GraphQl\Query\ResolverInterface`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Query/ResolverInterface.php) + +The first two interfaces provide a way to resolve multiple branches/leaves at once (known as batching), while the last one resolves one request at a time. We recommend using batch resolvers for queries because they improve performance by fetching information required to resolve multiple GraphQL requests with a single operation. + +## Query resolvers + +### BatchResolverInterface + +Batch resolvers gather GraphQL requests for the same field until there is no way to process the tree further without resolving previous requests. + +Consider the following example: + +```graphql +query ($filter: ProductAttributeFilterInput!) { + products (filter: $filter) { + items { + id + sku + related_products { + sku + related_products { + sku + } + } + } + total_count + } +} +``` + +The query loads a list of products, the SKUs of their related products, and then any secondary related product SKUs. + +Loading a list of related products individually for each product would be expensive performance-wise. With batch resolvers, you can load linked products for all products that were initially found, then group them by root products. After the `items` branch is resolved, a batch resolver for `related_products` will be called for the first product found. Instead of resolving the list right away, it will just add the first product to the list of products that require loading additional related products. After all the products from the `items` branch have been loaded, the lists of related products must be loaded. Then, `BatchResolverInterface::resolve()` executes with a gathered list of previous requests to `related_products` branches. At this point, the resolver is able to extract product DTOs from each GraphQL request, load all the product links, sort them by root products, and generate GraphQL values for each branch. After this is done, the same batching will take place when resolving child `related_products` branches. + +The following pseudo-code shows a `related_products` branch resolver: + +```php +class RelatedProducts implements BatchResolverInterface +{ + ... + + public function resolve(ContextInterface $context, Field $field, array $requests): BatchResponse + { + //Get the list of products we need to load related products for + $rootProductIds = array_map(function ($request) { return $request->getValue()['model']->getId(); }, $requests); + + //Load the links + $productLinks = $this->service->getRelatedProductLinks($rootProductIds); + + //Sort the links + $response = new BatchResponse(); + foreach ($requests as $request) { + $response->addResponse($request, $productLinks[$request->getValue()['model']->getId()]); + } + + return $response; + } +} +``` + +Each GraphQL request object must be assigned a [`\Magento\Framework\GraphQl\Query\Resolver\Value`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Value.php) result type or any type of data (mixed). This value takes in a callable function to its constructor that will be invoked at the latest possible time for the resolver to acquire its data. As a result, a list of items being resolved can be retrieved all at once by establishing a buffer that contains all relevant parent data to filter and fetch for the children list data. + +[`\Magento\RelatedProductGraphQl\Model\Resolver\Batch\AbstractLinkedProducts`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php) contains an example of batch resolver implementation. + +### BatchServiceContractResolverInterface + +Requests for this interface to resolve are gathered into batches in the same way as for `BatchResolverInterface`, except that the actual resolving is delegated to a batch service contract. The job of the `BatchServiceContractResolverInterface` resolver is to convert GraphQL requests into DTOs acceptable by the service contract, and then convert results returned by the contract into a GraphQL response. + +Consider the same example query: + +```graphql +query ($filter: ProductAttributeFilterInput!) { + products (filter: $filter) { + items { + id + sku + related_products { + sku + related_products { + sku + } + } + } + total_count + } +} +``` + +Here, we will delegate loading all related products to a service that accepts a list of root product IDs, and then returns individual lists for each. + +Pseudo-code for a GraphQL resolver delegating the work to a service contract may look like this: + +```php +class RelatedProductsResolver implements BatchServiceContractResolverInterface +{ + ... + + public function getServiceContract(): array + { + return [ProductLinksRetriever::class, 'getRelatedProducts']; + } + + public function convertToServiceArgument(ResolveRequestInterface $request) + { + return new RootProductCriteria($request->getValue()['model']->getId()); + } + + public function convertFromServiceResult($result, ResolveRequestInterface $request) + { + return $result->getLinkedProducts(); + } +} +``` + +The `getServiceContract()` method points to the service contract to be used. + +The `convertToServiceArgument()` method converts GraphQL requests to a criteria item to be passed in a list as the argument to the contract. Remember that batch service contract methods must accept a single argument: a list (array) of criteria objects. + +The `convertFromServiceResult()` method converts one of the result items into a GraphQL response (a [`\Magento\Framework\GraphQl\Query\Resolver\Value`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Value.php) instance or an array). Remember that batch service contracts must return result items in the same order as were the criteria items passed as the method's list argument. + +The batch service contract used in the example would look something like this: + +```php +class ProductLinksRetriever +{ + ... + + /** + * @param RootProductCriteria[] $criteriaList + * @return RelatedProductsFound[] + */ + public function getRelatedProducts(array $criteriaList): array + { + .... + } +} + +class RootProductCriteria +{ + .... + + public function __construct(int $rootProductId) + { + $this->productId = $rootProductId; + } + + public function getRootProductId(): int + { + return $this->productId; + } +} + +class RelatedProductsFound +{ + .... + + public function getLinkedProducts(): array + { + .... + } + + public function getRootProductId(): int + { + .... + } +} +``` + +A real example can be found at [\Magento\CatalogGraphQl\Model\Resolver\Product\BatchProductLinks]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php) + +### ResolverInterface + +This interface resolves one branch or leaf at a time. It returns [`\Magento\Framework\GraphQl\Query\Resolver\Value`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Value.php) or any type of data (mixed). This value takes in a callable function to its constructor that will be invoked at the latest possible time for the resolver to acquire its data. As a result, a list of items being resolved can be retrieved all at once by establishing a buffer that contains all relevant parent data to filter and fetch for the children list data. + +You can view an example inside the [`\Magento\BundleGraphQl\Model\Resolver\BundleItemLinks`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/BundleGraphQl/Model/Resolver/BundleItemLinks.php) resolver. This resolver takes each bundle option ID and its corresponding parent product ID and stores them in a collection's filter buffer (in this case, using the [`\Magento\BundleGraphQl\Model\Resolver\Links\Collection::addIdFilters()`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php#L62-L70) function). Each resolver then returns a callable that invokes this collection. The collection caches the result of all link entities it fetched for all the option_id/parent_id combinations. This fetch only needs to occur once for the whole `BundleItemLink` list, and each resulting callable that is invoked for every link in the list returns an item from the collections cached result. + +A `Value` object wraps a callable object, and you can use [`\Magento\Framework\GraphQl\Query\Resolver\ValueFactory`]({{ site.mage2bloburl }}/{{ page.guide_version }}/lib/internal/Magento/Framework/GraphQl/Query/Resolver/ValueFactory.php) to create a value. + +## Mutation requirements + +Like queries, mutations are also defined within the `/etc/schema.graphqls` file. + +### Mutation syntax + +```text +type Mutation { + mutationQueryName(inputParamName: MutationQueryInputType, inputParamName2: MutationQueryInputType2, ...): MutationQueryOutput @resolver(class: "Magento\\\\Model\\Resolver\\MutationResolverModel") @doc(description:"Mutation query description") +} +``` + +Syntax option | Description +--- | --- +`mutationQueryName` | The name of mutation +`inputParamName` | Input parameters for the mutation (optional) +`MutationQueryInputType` | The type of input parameter, such as `String`, `Int`, or a custom type, like `MyCustomInput` +`MutationQueryOutput` | The mutation's result type, such as `String`, `Int`, or a custom type, like `MyCustomOutput` +`@resolver(class)` | The class of the resolver +`@doc(description)` | Describes the purpose of the mutation +`@deprecated(reason: "description")` | Use `@deprecated` to mark a query, mutation, or attribute as deprecated + +### Resolver class + +Use the following sample code as a template for the GraphQL resolver mutation class: + +```php +\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * @inheritdoc + */ +class MutationResolverModel implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + // resolver functionality ... + } +} +``` + +### Example usage + +The mutation query below creates an empty cart and returns a cart unique identifier + +```text +type Mutation { + createEmptyCart: String @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\CreateEmptyCart") @doc(description:"Creates an empty shopping cart for a guest or logged in user") +} +``` + +Notice that a `MutationQueryInput` parameter is not specified here and `MutationQueryOutput` is defined as `String` type. + +The mutation to create a customer's account is more complex: + +```text +type Mutation { + createCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") +} +``` + +The `!` character indicates `CustomerInput` is a required input parameter. `CustomerInput` is defined as follows: + +```text +input CustomerInput { + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String @doc(description: "The customer's email address. Required") + date_of_birth: String @doc(description: "The customer's date of birth.") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + gender: Int @doc(description: "The customer's gender(Male - 1, Female - 2)") + password: String @doc(description: "The customer's password") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") +} +``` + +{:.bs-callout-info} +In keeping with current security and privacy best practices, be sure you are aware of any potential legal and security risks associated with the storage of customers’ full date of birth (month, day, year) along with other personal identifiers, such as full name, before collecting or processing such data. + +The `createCustomer` mutation returns `CustomerOutput` object + +```text +type CustomerOutput { + customer: Customer! +} +``` + +The `customer` parameter of the `CustomerOutput` object is a type of `Customer` object: + +```text +type Customer @doc(description: "Customer defines the customer name and address and other details") { + created_at: String @doc(description: "Timestamp indicating when the account was created") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String @doc(description: "The customer's email address. Required") + default_billing: String @doc(description: "The ID assigned to the billing address") + default_shipping: String @doc(description: "The ID assigned to the shipping address") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\IsSubscribed") + addresses: [CustomerAddress] @doc(description: "An array containing the customer's shipping and billing addresses") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") +} + +{:.bs-callout-info} +In keeping with current security and privacy best practices, be sure you are aware of any potential legal and security risks associated with the storage of customers’ full date of birth (month, day, year) along with other personal identifiers, such as full name, before collecting or processing such data. + +The following example shows the `createCustomer` mutation in action: + +```text +mutation { + createCustomer( + input: { + firstname: "John" + lastname: "Doe" + email: "j.doe@example.com" + password: "1w2E3R456" + is_subscribed: true + } + ) { + customer { + firstname + lastname + email + is_subscribed + } + } +} +``` + +A sample response: + +```json +{ + "data": { + "createCustomer": { + "customer": { + "firstname": "John", + "lastname": "Doe", + "email": "j.doe@example.com", + "is_subscribed": true + } + } + } +} +``` diff --git a/src/guides/v2.3/graphql/functional-testing.md b/src/guides/v2.3/graphql/functional-testing.md new file mode 100644 index 00000000000..c1dd6fd07eb --- /dev/null +++ b/src/guides/v2.3/graphql/functional-testing.md @@ -0,0 +1,499 @@ +--- +group: graphql +title: GraphQL functional testing +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +Magento provides API functional tests that can verify extension points in GraphQL. These tests serve as an example for exposing new queries via GraphQL. + +## Creating a new GraphQL functional test + +All GraphQL functional tests should be located in the `dev/tests/api-functional/testsuite/Magento/GraphQl/` directory and inherit from the generic test case `Magento\TestFramework\TestCase\GraphQlAbstract`. It defines the `graphQlQuery()` and `graphQlMutation()` methods, which should be used to perform Web API calls from tests. + +The following test verifies that the schema returns the correct attribute type, given the `attribute_code` and corresponding `entity_type`. + +```php +namespace Magento\GraphQl\Catalog; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductAttributeTypeTest extends GraphQlAbstract +{ + public function testAttributeTypeResolver() + { + $query + = <<graphQlQuery($query); + $expectedAttributeCodes = [ + 'description', + 'status', + 'special_price', + 'disable_auto_group_change', + 'special_price' + ]; + $entityType = [ + 'catalog_product', + 'catalog_product', + 'catalog_product', + 'customer', + \Magento\Catalog\Api\Data\ProductInterface::class + ]; + $attributeTypes = ['String', 'Int', 'Float','Boolean', 'Float']; + $inputTypes = ['textarea', 'select', 'price', 'boolean', 'price']; + $this->assertAttributeType($attributeTypes, $expectedAttributeCodes, $entityType, $inputTypes, $response); + } +``` + +## Using the default GraphQlQueryTest + +The `\Magento\GraphQl\TestModule\GraphQlQueryTest.php` test case uses two test modules to determine whether the mechanisms for GraphQL extensibility work as expected. It illustrates best practices for extending an existing GraphQL endpoint. + +* `TestModuleGraphQlQuery` - This bare-bones module defines a `testItem` endpoint with the queryable attributes `item_id` and `name`. It's located at `/dev/tests/api-functional/_files/TestModuleGraphQlQuery`. +* `TestModuleGraphQlQueryExtension` - This module extends `TestModuleGraphQlQuery`, adding the `integer_list` extension attribute. It's located at `/dev/tests/api-functional/_files/TestModuleGraphQlQueryExtension`. + +## Creating fixtures + +Fixtures, which are part of the testing framework, prepare preconditions in the system for further testing. For example, when you test the ability to add a product to the shopping cart, the precondition is that a product must be available for testing. + +A fixture consists of two files: + +* The fixture file, which defines the test +* A rollback file, which reverts the system to the state before the test was run + + {:.bs-callout-info} +Each fixture should have a corresponding rollback file. + +Magento provides fixtures in the `dev/tests/integration/testsuite/Magento//_files` directory. Use these fixtures whenever possible. When you create your own fixture, also create a proper rollback. + +### Fixture files + +The following fixture creates a simple product with predefined attributes. + +```php +get(ProductFactory::class); +$product = $productFactory->create(); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_VIRTUAL) + ->setId(21) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Virtual Product') + ->setSku('virtual-product') + ->setPrice(10) + ->setTaxClassId(0) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] + ); +/** @var ProductResource $productResource */ +$productResource = Bootstrap::getObjectManager()->create(ProductResource::class); +$productResource->save($product); +``` + +To use this fixture in a test, add it to the test's annotation in the following manner: + +```php + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_virtual.php + */ + public function testAddVirtualProductToShoppingCart() + { + // Test body + } +``` + +You can also invoke multiple fixtures: + +```php + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testSetNewBillingAddressByRegisteredCustomer() + { + // Test body + } +``` + +The specified fixtures will now execute on every test run. + +### Rollback files + +Every fixture should have a rollback file. A rollback is a set of operations that remove changes introduced by the fixture from the system once the test is completed. + +The rollback filename should correspond to the original fixture filename postfixed by `_rollback` keyword. For example, if the fixture file name is `virtual_product.php`, name the rollback file `virtual_product_rollback.php`. + +The following fixture rollback removes the newly-created product from the database. + +```php +get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$productRepository = Bootstrap::getObjectManager() + ->create(ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('virtual-product', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $exception) { + //Product already removed +} catch (StateException $exception) { +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); +``` + +### Fixture configs + +Use the `@magentoConfigFixture` annotation to set a custom config value. It supports a `store` scope only. + +#### Syntax + +```php +/** + * @magentoConfigFixture _store + */ +``` + +where + +* `` - Store code. See the `store`.`code` database field value. +* `` - Config key. See `core_config_data`.`path` +* `` - Config value. See `core_config_data`.`value` + + {:.bs-callout-info} +`@magentoConfigFixture` does not require a roll-back. + +#### Example usage + +The following example sets a store-scoped value `1` for the config key `checkout/options/enable_agreements` for the `default` store in the `GetActiveAgreement()` test: + +```php + /** + * @magentoConfigFixture default_store checkout/options/enable_agreements 1 + */ + public function testGetActiveAgreement() + { + ... + } +``` + +`@magentoConfigFixture` performs the following action as a background process before test execution: + +```sql +INSERT INTO `core_config_data` (scope`, `scope_id`, `path`, `value`) +VALUES + ('stores', 1, 'checkout/options/enable_agreements', '1'); +``` + +The fixture automatically removes the `checkout/options/enable_agreements` config key from the database after the test has been completed. + +## Defining expected exceptions + +Your functional tests should include events that cause exceptions. Since your tests expect an exception to occur, set up your tests so that they elicit the proper responses. You can define expected exception messages either in: + +* The body of the test +* The test function annotation + +{:.bs-callout-tip} +We recommend that you declare expected exceptions in the test method body, as declaring expected exceptions with annotations has been deprecated in PHPUnit 8. Existing tests that use annotations will have to be updated when Magento requires that version of PHPUnit or higher. + +### Exception messages in the body of a test + +The following examples show two ways you can use the `expectExceptionMessage` function to define an expected exception message. + +```php +public function testMyExceptionTest() +{ + ... + + self::expectExceptionMessage("Expected exception message goes here..."); + + ... +} + +``` + +or + +```php +public function testMyExceptionTest() +{ + ... + + $this->expectExceptionMessage("Expected exception message goes here..."); + + ... +} +``` + +{:.bs-callout-info} +Define the exception message before invoking logic that generates the exception. + +As an example, consider the case where Customer A tries to retrieve information about Customer B's cart. In this situation, Customer A gets this error: + +```terminal +The current user cannot perform operations on cart "XXXXX" +``` + +`XXXXX` is the unique ID of Customer B's cart. + +The following sample shows how to cover this scenario using an `expectExceptionMessage` function: + +```php + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_items_saved.php + */ + public function testGetCartFromAnotherCustomer() + { + $reservedOrderId = 'test_order_item_with_items'; + $this->quoteResource->load( + $this->quote, + $reservedOrderId, + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $query = $this->prepareGetCartQuery($maskedQuoteId); + self::expectExceptionMessage("The current user cannot perform operations on cart \"$maskedQuoteId\""); + $this->graphQlQuery($query); + } +``` + +### Exception messages in the annotation of a test function + +You can also use a predefined directive such as `@expectedExceptionMessage` as an alternative way to call the `expectExceptionMessage` method: + +```php + /** + * @expectedException \Exception + * @expectedExceptionMessage Expected exception message goes here... + */ +``` + +In the following query, a customer provides an incorrect cart ID while trying to retrieve information about his own cart. + +**Query:** + +```text +{ + cart(cart_id: "YYYYY") { + items { + __typename + id + qty + } + } +} +``` + +**Result:** + +```json +{ + "errors": [ + { + "message": "Could not find a cart with ID \"YYYYY\"", + "category": "graphql-no-such-entity", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": [ + "cart" + ] + } + ], + "data": { + "cart": null + } +} +``` + +The `@expectExceptionMessage` annotation provides the text for the exception in this test. + +```php + /** + * @expectedException \Exception + * @expectedExceptionMessage Could not find a cart with ID "non_existent_masked_id" + */ + public function testGetNonExistentCart() + { + $maskedQuoteId = 'non_existent_masked_id'; + $query = $this->prepareGetCartQuery($maskedQuoteId); + + $this->graphQlQuery($query); + } +``` + +Use the following functions to cover expected exceptions: + +* `expectException` +* `expectExceptionCode` +* `expectExceptionMessage` +* `expectExceptionMessageRegExp` +* `expectExceptionObject` + +## Run functional tests + +### Configure your instance + +1. Change directories to `dev/tests/api-functional/` and copy the `phpunit_graphql.xml.dist` file to `phpunit_graphql.xml`. + + ```bash + cp phpunit_graphql.xml.dist phpunit_graphql.xml + ``` + +1. Edit `phpunit_graphql.xml` to set values for the TESTS_BASE_URL, TESTS_WEBSERVICE_USER, TESTS_WEBSERVICE_APIKEY options: + + ```xml + ... + + + + + + + ... + ``` + +### Run all tests in a API functional test suite + +**Syntax:** + +```bash +vendor/bin/phpunit -c dev/tests/api-functional/phpunit_graphql.xml dev/tests/api-functional/testsuite///.php +``` + +**Example:** + +To run all tests from [dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php]({{ site.mage2bloburl }}/2.3.1/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php), run the following command: + +```bash +vendor/bin/phpunit -c dev/tests/api-functional/phpunit_graphql.xml dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php +``` + +### Run a single test in a API functional test suite + +**Syntax:** + +```bash +vendor/bin/phpunit -c dev/tests/api-functional/phpunit_graphql.xml --filter dev/tests/api-functional/testsuite///.php +``` + +**Example:** + +To run `testGenerateCustomerValidToken` test from [dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php]({{ site.mage2bloburl }}/2.3.1/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php), run the following command: + +```bash +vendor/bin/phpunit -c dev/tests/api-functional/phpunit_graphql.xml --filter testGenerateCustomerValidToken dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/GenerateCustomerTokenTest.php +``` + +### Run a selected group of tests in an API functional test suite + +Use the `@group` directive in the test annotation to add the ability to run a group tests. + +**Syntax:** + +```bash +vendor/bin/phpunit -c dev/tests/api-functional/phpunit_graphql.xml --group dev/tests/api-functional/testsuite///.php +``` + +**Example:** + +The `testGetCartTotalsWithNoAddressSet` test is marked with `@group recent`: + +```php +/graphql` in the URL bar of your IDE or extension. You can use the browser in the right column to determine how to set up a query or mutation. Examples are also available throughout the Magento GraphQL documentation. + +The following image shows a sample query, its response, and the GraphQL browser: + +![GraphiQL browser]({{ page.baseurl }}/graphql/images/graphql-browser.png) diff --git a/src/guides/v2.3/graphql/interfaces/bundle-product.md b/src/guides/v2.3/graphql/interfaces/bundle-product.md new file mode 100644 index 00000000000..d5c2fcda953 --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/bundle-product.md @@ -0,0 +1,310 @@ +--- +group: graphql +title: Bundle product data types +redirect_from: + - /guides/v2.3/graphql/reference/bundle-product.html + - /guides/v2.3/graphql/product/bundle-product.html +--- + +The `BundleProduct` data type implements the following interfaces: + +- [ProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html) +- [PhysicalProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html#PhysicalProductInterface) +- [CustomizableProductInterface]({{page.baseurl}}/graphql/interfaces/customizable-option-interface.html) + +Attributes that are specific to bundle products can be used when performing a [`products`]({{page.baseurl}}/graphql/queries/products.html) query. + +## BundleProduct object + +The `BundleProduct` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`dynamic_price` | Boolean | Indicates whether the bundle product has a dynamic price +`dynamic_sku` | Boolean | Indicates whether the bundle product has a dynamic SKU +`dynamic_weight` | Boolean | Indicates whether the bundle product has a dynamically calculated weight +`items` | [BundleItem] | An array containing information about individual bundle items +`price_view` | PriceViewEnum | One of PRICE_RANGE or AS_LOW_AS +`ship_bundle_items` | ShipBundleItemsEnum | Indicates whether to ship bundle items TOGETHER or SEPARATELY + +## BundleItem object + +The `BundleItem` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`option_id` | Int | An ID assigned to each type of item in a bundle product +`options` | [BundleItemOption] | An array of additional options for this bundle item +`position` | Int | The relative position of this item compared to the other bundle items +`required` | Boolean | Indicates whether the item must be included in the bundle +`sku` | String | The SKU of the bundle product +`title` | String | The display name of the item +`type` | String | The input type that the customer uses to select the item. Examples include radio button and checkbox. + +## BundleItemOption object + +The `BundleItemOption` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`can_change_quantity` | Boolean | Indicates whether the customer can change the number of items for this option +`id` | Int | The ID assigned to the bundled item option +`is_default` | Boolean | Indicates whether this option is the default option +`label` | String | The text that identifies the bundled item option +`position` | Int | When a bundle item contains multiple options, the relative position of this option compared to the other options +`price_type` | PriceTypeEnum | One of FIXED, PERCENT, or DYNAMIC +`price` | Float | The price of the selected option +`product` | [ProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html) | Contains details about this product option +`qty` | Float | Deprecated. Use `quantity` instead +`quantity` | Float | Indicates the quantity of this specific bundle item + +## Sample Query + +The following query returns information about bundle product `24-WG080`, which is defined in the sample data. + +```graphql +{ + products(filter: {sku: + {eq: "24-WG080"} + }) + { + items{ + sku + __typename + id + name + ... on BundleProduct { + dynamic_sku + dynamic_price + dynamic_weight + price_view + ship_bundle_items + items { + option_id + title + required + type + position + sku + options { + id + quantity + position + is_default + price + price_type + can_change_quantity + label + product { + id + name + sku + __typename + } + } + } + } + } + } +} +``` + +{% collapsible Response %} + +```json +{ + "data": { + "products": { + "items": [ + { + "sku": "24-WG080", + "__typename": "bundle", + "id": 46, + "name": "Sprite Yoga Companion Kit", + "dynamic_sku": true, + "dynamic_price": true, + "dynamic_weight": true, + "price_view": "PRICE_RANGE", + "ship_bundle_items": "TOGETHER", + "items": [ + { + "option_id": 1, + "title": "Sprite Stasis Ball", + "required": true, + "type": "radio", + "position": 1, + "sku": "24-WG080", + "options": [ + { + "id": 1, + "quantity": 1, + "position": 1, + "is_default": true, + "price": 0, + "price_type": "FIXED", + "can_change_quantity": true, + "label": "Sprite Stasis Ball 55 cm", + "product": { + "id": 26, + "name": "Sprite Stasis Ball 55 cm", + "sku": "24-WG081-blue", + "__typename": "simple" + } + }, + { + "id": 2, + "quantity": 1, + "position": 2, + "is_default": false, + "price": 0, + "price_type": "FIXED", + "can_change_quantity": true, + "label": "Sprite Stasis Ball 65 cm", + "product": { + "id": 29, + "name": "Sprite Stasis Ball 65 cm", + "sku": "24-WG082-blue", + "__typename": "simple" + } + }, + { + "id": 3, + "quantity": 1, + "position": 3, + "is_default": false, + "price": 0, + "price_type": "FIXED", + "can_change_quantity": true, + "label": "Sprite Stasis Ball 75 cm", + "product": { + "id": 32, + "name": "Sprite Stasis Ball 75 cm", + "sku": "24-WG083-blue", + "__typename": "simple" + } + } + ] + }, + { + "option_id": 2, + "title": "Sprite Foam Yoga Brick", + "required": true, + "type": "radio", + "position": 2, + "sku": "24-WG080", + "options": [ + { + "id": 4, + "quantity": 1, + "position": 1, + "is_default": true, + "price": 0, + "price_type": "FIXED", + "can_change_quantity": true, + "label": "Sprite Foam Yoga Brick", + "product": { + "id": 21, + "name": "Sprite Foam Yoga Brick", + "sku": "24-WG084", + "__typename": "simple" + } + } + ] + }, + { + "option_id": 3, + "title": "Sprite Yoga Strap", + "required": true, + "type": "radio", + "position": 3, + "sku": "24-WG080", + "options": [ + { + "id": 5, + "quantity": 1, + "position": 1, + "is_default": true, + "price": 0, + "price_type": "FIXED", + "can_change_quantity": true, + "label": "Sprite Yoga Strap 6 foot", + "product": { + "id": 33, + "name": "Sprite Yoga Strap 6 foot", + "sku": "24-WG085", + "__typename": "simple" + } + }, + { + "id": 6, + "quantity": 1, + "position": 2, + "is_default": false, + "price": 0, + "price_type": "FIXED", + "can_change_quantity": true, + "label": "Sprite Yoga Strap 8 foot", + "product": { + "id": 34, + "name": "Sprite Yoga Strap 8 foot", + "sku": "24-WG086", + "__typename": "simple" + } + }, + { + "id": 7, + "quantity": 1, + "position": 3, + "is_default": false, + "price": 0, + "price_type": "FIXED", + "can_change_quantity": true, + "label": "Sprite Yoga Strap 10 foot", + "product": { + "id": 35, + "name": "Sprite Yoga Strap 10 foot", + "sku": "24-WG087", + "__typename": "simple" + } + } + ] + }, + { + "option_id": 4, + "title": "Sprite Foam Roller", + "required": true, + "type": "radio", + "position": 4, + "sku": "24-WG080", + "options": [ + { + "id": 8, + "quantity": 1, + "position": 1, + "is_default": true, + "price": 0, + "price_type": "FIXED", + "can_change_quantity": true, + "label": "Sprite Foam Roller", + "product": { + "id": 22, + "name": "Sprite Foam Roller", + "sku": "24-WG088", + "__typename": "simple" + } + } + ] + } + ] + } + ] + } + } +} +``` + +{% endcollapsible %} + +## Related topics + +- [addBundleProductsToCart mutation]({{page.baseurl}}/graphql/mutations/add-bundle-products.html) diff --git a/src/guides/v2.3/graphql/interfaces/category-interface.md b/src/guides/v2.3/graphql/interfaces/category-interface.md new file mode 100644 index 00000000000..b7096cff383 --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/category-interface.md @@ -0,0 +1,67 @@ +--- +group: graphql +title: CategoryInterface attributes +redirect_from: + - /guides/v2.3/graphql/product/category-interface.html +--- + +`CategoryInterface` defines attributes that can be returned in the [`category` query]({{page.baseurl}}/graphql/queries/category.html) and the [`products` query]({{page.baseurl}}/graphql/queries/products.html). + +## CategoryInterface attributes + +The following table defines the `CategoryInterface` attributes and objects. + +Attribute | Type | Description +--- | --- | --- +`breadcrumbs` | [Breadcrumb] | A Breadcrumb object contains information the categories that comprise the breadcrumb trail for the specified category +`canonical_url` | String | The relative canonical URL. This value is returned only if the system setting **Use Canonical Link Meta Tag For Categories** is enabled +`cms_block` | CmsBlock | Contains a category CMS block. This attribute is defined in the `CatalogCmsGraphQl` module +`created_at` | String | Timestamp indicating when the category was created +`default_sort_by` | String | The attribute to use for sorting +`description` | String | An optional description of the category +`id` | Int | An ID that uniquely identifies the category +`level` | Int | Indicates the depth of the category within the tree +`name` | String | The display name of the category +`path_in_store` | String | Category path in the store +`path` | String | The path to the category, as a string of category IDs, separated by slashes (/). For example, 1/2/20 +`position` | Int | The position of the category relative to other categories at the same level in tree +`product_count` | Int | The number of products in the category +`products()` | CategoryProducts | The list of products assigned to the category +`updated_at` | String | Timestamp indicating when the category was updated +`url_key` | String | The URL key assigned to the category +`url_path` | String | The URL path assigned to the category + +### Breadcrumb object + +A breadcrumb trail is a set of links that shows customers where they are in relation to other pages in the +store. + +Attribute | Data type | Description +--- | --- | --- +`category_id` | Int | An ID that uniquely identifies the category +`category_level` | Int | Indicates the depth of the category within the tree +`category_name` | String | The display name of the category +`category_url_key` | String | The url key assigned to the category +`category_url_path` | String | The url path assigned to the category + +### CategoryProducts object + +The `products` attribute can contain the following attributes: + +Attribute | Data type | Description +--- | --- | --- +`currentPage` | Int | Specifies which page of results to return. The default value is 1 +`pageSize` | Int | Specifies the maximum number of results to return at once. This attribute is optional. The default value is 20 +`sort` | `ProductAttributeSortInput` | Specifies which attribute to sort on, and whether to return the results in ascending or descending order. [Searches and pagination in GraphQL]({{ page.baseurl }}/graphql/queries/index.html) describes sort orders + +The `CategoryProducts` object contains the following attributes: + +Attribute | Data type | Description +--- | --- | --- +`items` | [ProductInterface] | An array of products that are assigned to the category. See [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html) for more information +`page_info` | `SearchResultPageInfo` | An object that includes the `page_info` and `currentPage` values specified in the query +`total_count` | Int | The number of products returned + +### CmsBlock attributes + +{% include graphql/cms-block-object.md %} diff --git a/src/guides/v2.3/graphql/interfaces/configurable-product.md b/src/guides/v2.3/graphql/interfaces/configurable-product.md new file mode 100644 index 00000000000..bd31250cc76 --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/configurable-product.md @@ -0,0 +1,809 @@ +--- +group: graphql +title: Configurable product data types +redirect_from: + - /guides/v2.3/graphql/reference/configurable-product.html + - /guides/v2.3/graphql/product/configurable-product.html +--- + +The `ConfigurableProduct` data type implements the following interfaces: + +- [ProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html) +- [PhysicalProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html#PhysicalProductInterface) +- [CustomizableProductInterface]({{page.baseurl}}/graphql/interfaces/customizable-option-interface.html) + +Attributes that are specific to configurable products can be used when performing a [`products`]({{page.baseurl}}/graphql/queries/products.html) query. + +## ConfigurableProduct object + +The `ConfigurableProduct` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`configurable_options` | [[ConfigurableProductOptions]](#configProdOptions) | An array of linked simple product items +`variants` | ConfigurableVariant | An array of variants of products + +### ConfigurableVariant object + +The `ConfigurableVariant` object contains the following attributes: + +Field | Type | Description +--- | --- | --- +`attributes` | ConfigurableAttributeOption | ConfigurableAttributeOption contains the value_index (and other related information) assigned to a configurable product option +`product` | SimpleProduct | An array of linked simple products + +### ConfigurableAttributeOption object + +The `ConfigurableAttributeOption` object contains the following attributes: + +Field | Type | Description +--- | --- | --- +`code` | String | The ID assigned to the attribute +`label` | String | A string that describes the configurable attribute option +`value_index` | Int | A unique index number assigned to the configurable product option + +### ConfigurableProductOptions {#configProdOptions} + +The `ConfigurableProductOptions` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`attribute_code` | String | A string that identifies the attribute +`attribute_id` | String | The ID assigned to the attribute +`id` | Int | The configurable option ID number assigned by the system +`label` | String | A string that describes the configurable product option. It is displayed on the UI. +`position` | Int | A number that indicates the order in which the attribute is displayed +`product_id` | Int | This is the same as a product's 'id' field +`use_default` | Boolean | Indicates whether the option is the default +`values` | [[ConfigurableProductOptionsValues]](#configProdOptionsValues) | An array that defines the `value_index` codes assigned to the configurable product + +### ConfigurableProductOptionsValues {#configProdOptionsValues} + +The `ConfigurableProductOptionsValues` object contains the following attribute: + +Attribute | Type | Description +--- | --- | --- +`default_label` | String | The label of the product on the default store +`label` | String | The label of the product +`store_label` | String | The label of the product on the current store +`swatch_data` | [SwatchDataInterface](#swatchDataInterface) | Details about swatches that can be displayed for configurable product options +`use_default_value` | Boolean | Indicates whether to use the default_label +`value_index` | Int | A unique index number assigned to the configurable product option + +### SwatchDataInterface {#swatchDataInterface} + +Swatches allow the shopper to view the color, texture, or other visual aspect of a configurable product. Magento displays these options as color, graphic, or text swatches. + +The following data types implement `SwatchDataInterface`: + +- `ColorSwatchData` +- `ImageSwatchData` +- `TextSwatchData` + +Attribute | Type | Description +--- | --- | --- +`value` | String | The value of swatch item. The value is a hexadecimal color code, such as `#000000` (black), for color swatches, the image link for image swatches, or the display text for text swatches +`thumbnail` | String | Applicable to image swatches only. The URL to thumbnail swatch image + +## Sample queries + +Add the following inline fragment to the output section of your `products` query to return information specific to configurable products: + +```text +... on ConfigurableProduct { + configurable_options { + + } +} +``` + +### Extended example + +The following `products` query returns `ConfigurableProduct` information about the `WH01` configurable product, which is defined in the sample data. + +**Request:** + +```graphql +{ + products(filter: { sku: { eq: "WH01" } }) { + items { + id + attribute_set_id + name + sku + __typename + price_range{ + minimum_price{ + regular_price{ + value + currency + } + } + } + categories { + id + } + ... on ConfigurableProduct { + configurable_options { + id + attribute_id + label + position + use_default + attribute_code + values { + value_index + label + } + product_id + } + variants { + product { + id + name + sku + attribute_set_id + ... on PhysicalProductInterface { + weight + } + price_range{ + minimum_price{ + regular_price{ + value + currency + } + } + } + } + attributes { + label + code + value_index + } + } + } + } + } +} +``` + +**Response:** + +{% collapsible View response %} + +``` json +{ + "data": { + "products": { + "items": [ + { + "id": 1050, + "attribute_set_id": 9, + "name": "Mona Pullover Hoodlie", + "sku": "WH01", + "__typename": "configurable", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + }, + "categories": [ + { + "id": 2 + }, + { + "id": 8 + }, + { + "id": 24 + }, + { + "id": 34 + } + ], + "configurable_options": [ + { + "id": 147, + "attribute_id": "93", + "label": "Color", + "position": 1, + "use_default": false, + "attribute_code": "color", + "values": [ + { + "value_index": 53, + "label": "Green" + }, + { + "value_index": 56, + "label": "Orange" + }, + { + "value_index": 57, + "label": "Purple" + } + ], + "product_id": 1050 + }, + { + "id": 146, + "attribute_id": "160", + "label": "Size", + "position": 0, + "use_default": false, + "attribute_code": "size", + "values": [ + { + "value_index": 176, + "label": "XS" + }, + { + "value_index": 177, + "label": "S" + }, + { + "value_index": 178, + "label": "M" + }, + { + "value_index": 179, + "label": "L" + }, + { + "value_index": 180, + "label": "XL" + } + ], + "product_id": 1050 + } + ], + "variants": [ + { + "product": { + "id": 1035, + "name": "Mona Pullover Hoodlie-XS-Green", + "sku": "WH01-XS-Green", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Green", + "code": "color", + "value_index": 53 + }, + { + "label": "XS", + "code": "size", + "value_index": 176 + } + ] + }, + { + "product": { + "id": 1036, + "name": "Mona Pullover Hoodlie-XS-Orange", + "sku": "WH01-XS-Orange", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Orange", + "code": "color", + "value_index": 56 + }, + { + "label": "XS", + "code": "size", + "value_index": 176 + } + ] + }, + { + "product": { + "id": 1037, + "name": "Mona Pullover Hoodlie-XS-Purple", + "sku": "WH01-XS-Purple", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Purple", + "code": "color", + "value_index": 57 + }, + { + "label": "XS", + "code": "size", + "value_index": 176 + } + ] + }, + { + "product": { + "id": 1038, + "name": "Mona Pullover Hoodlie-S-Green", + "sku": "WH01-S-Green", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Green", + "code": "color", + "value_index": 53 + }, + { + "label": "S", + "code": "size", + "value_index": 177 + } + ] + }, + { + "product": { + "id": 1039, + "name": "Mona Pullover Hoodlie-S-Orange", + "sku": "WH01-S-Orange", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Orange", + "code": "color", + "value_index": 56 + }, + { + "label": "S", + "code": "size", + "value_index": 177 + } + ] + }, + { + "product": { + "id": 1040, + "name": "Mona Pullover Hoodlie-S-Purple", + "sku": "WH01-S-Purple", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Purple", + "code": "color", + "value_index": 57 + }, + { + "label": "S", + "code": "size", + "value_index": 177 + } + ] + }, + { + "product": { + "id": 1041, + "name": "Mona Pullover Hoodlie-M-Green", + "sku": "WH01-M-Green", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Green", + "code": "color", + "value_index": 53 + }, + { + "label": "M", + "code": "size", + "value_index": 178 + } + ] + }, + { + "product": { + "id": 1042, + "name": "Mona Pullover Hoodlie-M-Orange", + "sku": "WH01-M-Orange", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Orange", + "code": "color", + "value_index": 56 + }, + { + "label": "M", + "code": "size", + "value_index": 178 + } + ] + }, + { + "product": { + "id": 1043, + "name": "Mona Pullover Hoodlie-M-Purple", + "sku": "WH01-M-Purple", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Purple", + "code": "color", + "value_index": 57 + }, + { + "label": "M", + "code": "size", + "value_index": 178 + } + ] + }, + { + "product": { + "id": 1044, + "name": "Mona Pullover Hoodlie-L-Green", + "sku": "WH01-L-Green", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Green", + "code": "color", + "value_index": 53 + }, + { + "label": "L", + "code": "size", + "value_index": 179 + } + ] + }, + { + "product": { + "id": 1045, + "name": "Mona Pullover Hoodlie-L-Orange", + "sku": "WH01-L-Orange", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Orange", + "code": "color", + "value_index": 56 + }, + { + "label": "L", + "code": "size", + "value_index": 179 + } + ] + }, + { + "product": { + "id": 1046, + "name": "Mona Pullover Hoodlie-L-Purple", + "sku": "WH01-L-Purple", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Purple", + "code": "color", + "value_index": 57 + }, + { + "label": "L", + "code": "size", + "value_index": 179 + } + ] + }, + { + "product": { + "id": 1047, + "name": "Mona Pullover Hoodlie-XL-Green", + "sku": "WH01-XL-Green", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Green", + "code": "color", + "value_index": 53 + }, + { + "label": "XL", + "code": "size", + "value_index": 180 + } + ] + }, + { + "product": { + "id": 1048, + "name": "Mona Pullover Hoodlie-XL-Orange", + "sku": "WH01-XL-Orange", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Orange", + "code": "color", + "value_index": 56 + }, + { + "label": "XL", + "code": "size", + "value_index": 180 + } + ] + }, + { + "product": { + "id": 1049, + "name": "Mona Pullover Hoodlie-XL-Purple", + "sku": "WH01-XL-Purple", + "attribute_set_id": 9, + "weight": 1, + "price_range": { + "minimum_price": { + "regular_price": { + "value": 57, + "currency": "USD" + } + } + } + }, + "attributes": [ + { + "label": "Purple", + "code": "color", + "value_index": 57 + }, + { + "label": "XL", + "code": "size", + "value_index": 180 + } + ] + } + ] + } + ] + } + } +} +``` + +{% endcollapsible %} + +### Return swatch information + +The following query returns the color and text swatches assigned to configurable product `MJ06`. + +**Request:** + +```graphql +{ + products(filter: {sku: {eq: "MJ06"}}) { + items { + ... on ConfigurableProduct{ + configurable_options{ + values { + label + swatch_data{ + value + } + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "products": { + "items": [ + { + "configurable_options": [ + { + "values": [ + { + "label": "Blue", + "swatch_data": { + "value": "#1857f7" + } + }, + { + "label": "Green", + "swatch_data": { + "value": "#53a828" + } + }, + { + "label": "Purple", + "swatch_data": { + "value": "#ef3dff" + } + } + ] + }, + { + "values": [ + { + "label": "XS", + "swatch_data": { + "value": "XS" + } + }, + { + "label": "S", + "swatch_data": { + "value": "S" + } + }, + { + "label": "M", + "swatch_data": { + "value": "M" + } + }, + { + "label": "L", + "swatch_data": { + "value": "L" + } + }, + { + "label": "XL", + "swatch_data": { + "value": "XL" + } + } + ] + } + ] + } + ] + } + } +} +``` + +## Related topics + +- [addConfigurableProductsToCart mutation]({{page.baseurl}}/graphql/mutations/add-configurable-products.html) diff --git a/src/guides/v2.3/graphql/interfaces/customizable-option-interface.md b/src/guides/v2.3/graphql/interfaces/customizable-option-interface.md new file mode 100644 index 00000000000..52addda207d --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/customizable-option-interface.md @@ -0,0 +1,245 @@ +--- +group: graphql +title: CustomizableOptionInterface +redirect_from: + - /guides/v2.3/graphql/reference/customizable-option-interface.html + - /guides/v2.3/graphql/product/customizable-option-interface.html +--- + +Customizable options for a product provide a way to offer customers a selection of options with a variety of text, selection, and date input types. All product types can contain customizable options. + +`CustomizableOptionInterface` is defined in the `CatalogGraphQl` module, and its attributes can be used in any `products` query. This interface returns basic information about a customizable option and can be implemented by several types of configurable options: + +* Text area +* Checkbox +* Date picker +* Drop-down menu +* Text field +* File picker +* Multiple select box +* Radio buttons + +{:.bs-callout-info} +Magento has not implemented all possible customizable product options for GraphQL. + +`CustomizableOptionInterface` can contain the following attributes: + +Attribute | Type | Description +--- | --- | --- +`option_id` | Int | The ID assigned to the option +`required` | Boolean | Indicates whether the option is required +`sort_order` | Int | The order in which the option is displayed +`title` | String | The display name for this option + +## CustomizableAreaOption object + +`CustomizableAreaOption` contains information about a text area that is defined as part of a customizable option. + +Attribute | Type | Description +--- | --- | --- +`product_sku` | String | The Stock Keeping Unit of the base product +`value` | `CustomizableAreaValue` | An object that defines a text area + +### CustomizableAreaValue object + +`CustomizableAreaValue` defines the attributes of a product whose page contains a customized text area. + +Attribute | Type | Description +--- | --- | --- +`max_characters` | Int | The maximum number of characters that can be entered for this customizable option +`price_type` | PriceTypeEnum | FIXED, PERCENT, or DYNAMIC +`price` | Float | The price assigned to this option +`sku` | String | The Stock Keeping Unit for this option + +## CustomizableCheckboxOption object + +`CustomizableCheckboxOption` contains information about a set of checkbox values that are defined as part of a customizable option. + +Attribute | Type | Description +--- | --- | --- +`value` | `CustomizableCheckboxValue` | An array that defines a set of checkbox values + +### CustomizableCheckboxValue object + +`CustomizableCheckboxValue` defines the attributes of a product whose page contains a customized set of checkbox values. + +Attribute | Type | Description +--- | --- | --- +`option_type_id` | Int | The ID assigned to the value +`price_type` | PriceTypeEnum | FIXED, PERCENT, or DYNAMIC +`price` | Float | The price assigned to this option +`sku` | String | The Stock Keeping Unit for this option +`sort_order` | Int | The order in which the option is displayed +`title` | String | The display name for this option + +## CustomizableDateOption object + +`CustomizableDateOption` contains information about a date picker that is defined as part of a customizable option. + +Attribute | Type | Description +--- | --- | --- +`product_sku` | String | The Stock Keeping Unit of the base product +`value` | `CustomizableDateValue` | An object that defines a date field in a customizable option. + +### CustomizableDateValue object + +`CustomizableDateValue` defines the attributes of a product whose page contains a customized date picker. + +Attribute | Type | Description +--- | --- | --- +`price` | Float | The price assigned to this option +`price_type` | PriceTypeEnum | FIXED, PERCENT, or DYNAMIC +`sku` | String | The Stock Keeping Unit for this option + +## CustomizableDropDownOption object + +`CustomizableDropDownOption` contains information about a drop down menu that is defined as part of a customizable option. + +Attribute | Type | Description +--- | --- | --- +`value` | `CustomizableDropDownValue` | An array that defines the set of options for a drop down menu + +### CustomizableDropDownValue object + +`CustomizableDropDownValue` defines the attributes of a product whose page contains a customized drop down menu. + +Attribute | Type | Description +--- | --- | --- +`option_type_id` | Int | The ID assigned to the value +`price_type` | PriceTypeEnum | FIXED, PERCENT, or DYNAMIC +`price` | Float | The price assigned to this option +`sku` | String | The Stock Keeping Unit for this option +`sort_order` | Int | The order in which the option is displayed +`title` | String | The display name for this option + +## CustomizableFieldOption object + +`CustomizableFieldOption` contains information about a text field that is defined as part of a customizable option. + +Attribute | Type | Description +--- | --- | --- +`product_sku` | String | The Stock Keeping Unit of the base product +`value` | `CustomizableFieldValue` | An object that defines a text field + +### CustomizableFieldValue object + +`CustomizableFieldValue` defines the attributes of a product whose page contains a customized text field. + +Attribute | Type | Description +--- | --- | --- +`max_characters` | Int | The maximum number of characters that can be entered for this customizable option +`price_type` | PriceTypeEnum | FIXED, PERCENT, or DYNAMIC +`price` | Float | The price of the custom value +`sku` | String | The Stock Keeping Unit for this option + +## CustomizableFileOption object + +{:.bs-callout-info} +The `CustomizableFileOption` object is not supported. + +### CustomizableFileValue object + +{:.bs-callout-info} +The `CustomizableFileValue` object is not supported. + +## CustomizableMultipleOption object + +`CustomizableMultipleOption` contains information about a multiselect that is defined as part of a customizable option. + +Attribute | Type | Description +--- | --- | --- +`value` | `CustomizableMultipleValue` | An array that defines the set of options for a multiselect + +### CustomizableMultipleValue object + +`CustomizableMultipleValue` defines the price and sku of a product whose page contains a customized multiselect + +Attribute | Type | Description +--- | --- | --- +`option_type_id` | Int | The ID assigned to the value +`price_type` | PriceTypeEnum | FIXED, PERCENT, or DYNAMIC +`price` | Float | The price assigned to this option +`sku` | String | The Stock Keeping Unit for this option +`sort_order` | Int | The order in which the option is displayed +`title` | String | The display name for this option + +## CustomizableRadioOption object + +`CustomizableRadioOption` contains information about a set of radio buttons that are defined as part of a customizable option. + +Attribute | Type | Description +--- | --- | --- +`value` | `CustomizableRadioValue` | An array that defines a set of radio buttons + +### CustomizableRadioValue object + +`CustomizableRadioValue` defines the attributes of a product whose page contains a customized set of radio buttons. + +Attribute | Type | Description +--- | --- | --- +`option_type_id` | Int | The ID assigned to the value +`price_type` | PriceTypeEnum | FIXED, PERCENT, or DYNAMIC +`price` | Float | The price assigned to this option +`sku` | String | The Stock Keeping Unit for this option +`sort_order` | Int | The order in which the option is displayed +`title` | String | The display name for this option## CustomizableRadioOption object + +`CustomizableRadioOption` contains information about a set of radio buttons that are defined as part of a customizable option. + +Attribute | Type | Description +--- | --- | --- +`value` | `CustomizableRadioValue` | An array that defines a set of radio buttons + +## Example usage + +The following query returns information about the customizable options configured for the product with a `sku` of `xyz`. + +**Request:** + +```graphql +{ + products(filter: {sku: {eq: "xyz"}}) { + items { + id + name + sku + __typename + ... on CustomizableProductInterface { + options { + title + required + sort_order + option_id + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "products": { + "items": [ + { + "id": 1, + "name": "T-shirt", + "sku": "xyz", + "__typename": "SimpleProduct", + "options": [ + { + "title": "Image", + "required": false, + "sort_order": 1, + "option_id": 1 + } + ] + } + ] + } + } +} +``` diff --git a/src/guides/v2.3/graphql/interfaces/downloadable-product.md b/src/guides/v2.3/graphql/interfaces/downloadable-product.md new file mode 100644 index 00000000000..37909fd0bef --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/downloadable-product.md @@ -0,0 +1,161 @@ +--- +group: graphql +title: Downloadable product data types +redirect_from: + - /guides/v2.3/graphql/reference/downloadable-product.html + - /guides/v2.3/graphql/product/downloadable-product.html +--- + +The `DownloadableProduct` data type implements `ProductInterface` and `CustomizableProductInterface`. As a result, attributes that are specific to downloadable products can be used when performing a [`products`]({{page.baseurl}}/graphql/queries/products.html) query. + +## Downloadable product + +The `DownloadableProduct` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`downloadable_product_links` | [[DownloadableProductLinks]](#DownloadableProductLinks) | An array containing information about the links for this downloadable product +`downloadable_product_samples` | [[DownloadableProductSamples]](#DownloadableProductSamples) | An array containing information about samples of this downloadable product +`links_purchased_separately` | Int | A value of 1 indicates that each link in the array must be purchased separately +`links_title` | String | The heading above the list of downloadable products + +### DownloadableProductSamples object {#DownloadableProductSamples} + +The `DownloadableProductSamples` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`id` | Int | Deprecated. This attribute is not applicable for GraphQL +`sample_file` | String | Deprecated. Use `sample_url` instead +`sample_type` | DownloadableFileTypeEnum | Deprecated. Use `sample_url` instead +`sample_url` | String | The URL to the downloadable sample +`sort_order` | Int | A number indicating the sort order +`title` | String | The display name of the sample + +### DownloadableProductLinks object {#DownloadableProductLinks} + +The `DownloadableProductLinks` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`id` | Int | Deprecated. This information should not be exposed on frontend +`is_shareable` | Boolean | Deprecated. This attribute is not applicable for GraphQL +`link_type` | DownloadableFileTypeEnum | Deprecated. Use `sample_url` instead +`number_of_downloads` | Int | Deprecated. This attribute is not applicable for GraphQL +`price` | Float | The price of the downloadable product +`sample_file` | String | Deprecated. Use `sample_url` instead +`sample_type` | DownloadableFileTypeEnum | Deprecated. Use `sample_url` instead +`sample_url` | String | The URL to the downloadable sample +`sort_order` | Int | A number indicating the sort order +`title` | String | The display name of the link + +## Example usage + +Add the following inline fragment to the output section of your `products` query to return information specific to downloadable products: + +```text +... on DownloadableProduct { + items { + + } +} +``` + +The following query returns information about downloadable product `240-LV04`, which is defined in the sample data. + +**Request:** + +```graphql +{ + products(filter: { sku: { eq: "240-LV04" } }) { + items { + id + name + sku + __typename + price_range{ + minimum_price{ + regular_price{ + value + currency + } + } + } + ... on DownloadableProduct { + links_title + links_purchased_separately + + downloadable_product_links { + sample_url + sort_order + title + price + } + downloadable_product_samples { + title + sort_order + sample_url + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "products": { + "items": [ + { + "id": 47, + "name": "Beginner's Yoga", + "sku": "240-LV04", + "__typename": "downloadable", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 6, + "currency": "USD" + } + } + }, + "links_title": "Downloads", + "links_purchased_separately": 0, + "downloadable_product_links": [ + { + "sample_url": null, + "sort_order": 1, + "title": "Beginner's Yoga", + "price": 6 + } + ], + "downloadable_product_samples": [ + { + "title": "Trailer #1", + "sort_order": 1, + "sample_url": "/l/u/luma_background_-_model_against_fence_4_sec_.mp4" + }, + { + "title": "Trailer #2", + "sort_order": 1, + "sample_url": "/l/u/luma_background_-_model_against_fence_4_sec_.mp4" + }, + { + "title": "Trailer #3", + "sort_order": 1, + "sample_url": "/l/u/luma_background_-_model_against_fence_4_sec_.mp4" + } + ] + } + ] + } + } +} +``` + +## Related topics + +- [customerDownloadableProducts query]({{page.baseurl}}/graphql/queries/customer-downloadable-products.html) diff --git a/src/guides/v2.3/graphql/interfaces/gift-card-product.md b/src/guides/v2.3/graphql/interfaces/gift-card-product.md new file mode 100644 index 00000000000..21eaf676957 --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/gift-card-product.md @@ -0,0 +1,84 @@ +--- +group: graphql +title: Gift card product data types +ee_only: True +redirect_from: + - /guides/v2.3/graphql/reference/gift-card-product.html + - /guides/v2.3/graphql/product/gift-card-product.html +--- + +The `GiftCardProduct` data type defines properties of a gift card, including the minimum and maximum values and an array that contains the current and past values on the specific gift card + +It implements the following interfaces: + +- [ProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html) +- [PhysicalProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html#PhysicalProductInterface) +- [CustomizableProductInterface]({{page.baseurl}}/graphql/interfaces/customizable-option-interface.html) + +## GiftCardProduct object + +The `GiftCardProduct` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`allow_message` | Boolean | Indicates whether the customer can provide a message to accompany the gift card +`allow_open_amount` | Boolean | Indicates whether customers have the ability to set the value of the gift card +`giftcard_amounts` | [`GiftCardAmounts`] | An array that contains information about the values and ID of a gift card +`giftcard_type` | `GiftCardTypeEnum` | Either VIRTUAL, PHYSICAL, or COMBINED +`is_redeemable` | Boolean | Indicates whether the customer can redeem the value on the card for cash +`lifetime` | Int | The number of days after purchase until the gift card expires. A null value means there is no limit +`message_max_length` | Int | The maximum number of characters a gift card message can contain +`open_amount_max` | Float | The maximum acceptable value of an open amount gift card +`open_amount_min` | Float | The minimum acceptable value of an open amount gift card + +## GiftCardAmounts object + +The `GiftCardAmounts` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`attribute_id` | Int | An internal attribute ID +`value_id` | Int | An ID that is assigned to each unique gift card amount +`value` | Float | The value of the gift card +`website_value` | Float |The value of the gift card +`website_id` | Int | ID of the website that generated the gift card + +## Sample Query + +The following query returns information about gift card product `GiftCard25`. (It is not defined in the sample data.) + +```graphql +{ + products(filter: { sku: { eq: "GiftCard25" } }) { + items { + id + __typename + name + sku + ... on GiftCardProduct { + allow_message + message_max_length + allow_open_amount + open_amount_min + open_amount_max + is_returnable + is_redeemable + giftcard_type + giftcard_amounts { + value_id + website_id + value + attribute_id + website_value + } + } + } + } +} +``` + +## Related topics + +- [applyGiftCardToCart mutation]({{page.baseurl}}/graphql/mutations/apply-giftcard.html) +- [redeemGiftCardBalanceAsStoreCredit mutation]({{page.baseurl}}/graphql/mutations/redeem-giftcard-balance.html) +- [removeGiftCardFromCart mutation]({{page.baseurl}}/graphql/mutations/remove-giftcard.html) diff --git a/src/guides/v2.3/graphql/interfaces/grouped-product.md b/src/guides/v2.3/graphql/interfaces/grouped-product.md new file mode 100644 index 00000000000..c191df918d6 --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/grouped-product.md @@ -0,0 +1,33 @@ +--- +group: graphql +title: Grouped product data types +redirect_from: + - /guides/v2.3/graphql/reference/grouped-product.html + - /guides/v2.3/graphql/product/grouped-product.html +--- + +The `GroupedProduct` data type implements [ProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html) and [PhysicalProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html#PhysicalProductInterface). As a result, attributes that are specific to grouped products can be used when performing a [products]({{page.baseurl}}/graphql/queries/products.html) query. + +## GroupedProduct + +The `GroupedProduct` object contains the `[items]` array: + +Attribute | Type | Description +--- | --- | --- +`items` | [GroupedProductItem] | An array containing grouped product items + +## GroupedProductItem {#GroupedProductItem} + +The `GroupedProductItem` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`position` | Int | The relative position of this item compared to the other group items +`product` | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html) | The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes +`qty` | Float | The quantity of this grouped product item + +## Sample Query + +The following query returns information about downloadable product `24-WG085_Group`, which is defined in the sample data. + +{% include graphql/grouped-product-sample.md %} diff --git a/src/guides/v2.3/graphql/interfaces/product-interface-implementations.md b/src/guides/v2.3/graphql/interfaces/product-interface-implementations.md new file mode 100644 index 00000000000..ac783eb4de4 --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/product-interface-implementations.md @@ -0,0 +1,37 @@ +--- +group: graphql +title: Product interface implementations +redirect_from: + - /guides/v2.3/graphql/reference/product-interface-implementations.html + - /guides/v2.3/graphql/product/product-interface-implementations.html +--- + +Magento provides multiple product types, and most of these product types have specialized attributes that are not defined in the `ProductInterface`. + +Product type | Implements | Has product-specific attributes? +--- | --- | --- +[BundleProduct]({{ page.baseurl }}/graphql/interfaces/bundle-product.html) | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html), [PhysicalProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html#PhysicalProductInterface), [CustomizableProductInterface]({{ page.baseurl }}/graphql/interfaces/customizable-option-interface.html) | Yes +[ConfigurableProduct]({{ page.baseurl }}/graphql/interfaces/configurable-product.html) | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html), [PhysicalProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html#PhysicalProductInterface), [CustomizableProductInterface]({{ page.baseurl }}/graphql/interfaces/customizable-option-interface.html) | Yes +[DownloadableProduct]({{ page.baseurl }}/graphql/interfaces/downloadable-product.html) | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html), [CustomizableProductInterface]({{ page.baseurl }}/graphql/interfaces/customizable-option-interface.html) | Yes +[GiftCardProduct]({{ page.baseurl }}/graphql/interfaces/gift-card-product.html) | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html), [PhysicalProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html#PhysicalProductInterface),[CustomizableProductInterface]({{ page.baseurl }}/graphql/interfaces/customizable-option-interface.html) | Yes +[GroupedProduct]({{ page.baseurl }}/graphql/interfaces/grouped-product.html) | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html), [PhysicalProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html#PhysicalProductInterface), [CustomizableProductInterface]({{ page.baseurl }}/graphql/interfaces/customizable-option-interface.html) | Yes +[SimpleProduct]({{ page.baseurl }}/graphql/interfaces/simple-product.html) | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html), [PhysicalProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html#PhysicalProductInterface), [CustomizableProductInterface]({{ page.baseurl }}/graphql/interfaces/customizable-option-interface.html) | No +[VirtualProduct]({{ page.baseurl }}/graphql/interfaces/virtual-product.html) | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html), [CustomizableProductInterface]({{ page.baseurl }}/graphql/interfaces/customizable-option-interface.html) | No + +## Query for product-specific attributes + +To return attributes that are specific to a product type, append a structure similar to the following to the end of the `Products` output object: + +```text +... on { + items{ + + + ... + } + } +``` + +For example, to return `GroupedProduct` attributes, construct your query like this: + +{% include graphql/grouped-product-sample.md %} diff --git a/src/guides/v2.3/graphql/interfaces/product-interface.md b/src/guides/v2.3/graphql/interfaces/product-interface.md new file mode 100644 index 00000000000..4c896878261 --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/product-interface.md @@ -0,0 +1,300 @@ +--- +group: graphql +title: ProductInterface attributes +redirect_from: + - /guides/v2.3/graphql/product/product-interface.html +--- + +Any type that implements `ProductInterface` contains all the base attributes necessary for the frontend of the product model. +The `items` that are returned in a `ProductInterface` array can also contain attributes from resources external to the `CatalogGraphQl` module: + +- Custom and extension attributes defined in any attribute set +- The attribute is defined in the [PhysicalProductInterface](#PhysicalProductInterface) or [CustomizableOptionInterface]({{ page.baseurl }}/graphql/interfaces/customizable-option-interface.html) +- Product types that define their own implementation of `ProductInterface` including: + - [SimpleProduct]({{ page.baseurl }}/graphql/interfaces/simple-product.html) + - [BundleProduct]({{ page.baseurl }}/graphql/interfaces/bundle-product.html) + - [ConfigurableProduct]({{ page.baseurl }}/graphql/interfaces/configurable-product.html) + - [DownloadableProduct]({{ page.baseurl }}/graphql/interfaces/downloadable-product.html) + - [GiftCardProduct]({{ page.baseurl }}/graphql/interfaces/gift-card-product.html) + - [GroupedProduct]({{ page.baseurl }}/graphql/interfaces/grouped-product.html) + - [VirtualProduct]({{ page.baseurl }}/graphql/interfaces/virtual-product.html) + +## ProductInterface attributes + +The following table defines the `ProductInterface` attributes and objects. + +Attribute | Data type | Description +--- | --- | --- +`attribute_set_id` | Int | The attribute set assigned to the product +`canonical_url` | String | The relative canonical URL. This value is returned only if the system setting **Use Canonical Link Meta Tag For Products** is enabled +`categories` | [[CategoryInterface]]({{ page.baseurl }}/graphql/interfaces/category-interface.html) | The categories assigned to the product. See [CategoryInterface attributes]({{ page.baseurl }}/graphql/interfaces/category-interface.html) for more information +`country_of_manufacture` | String | The product's country of origin +`created_at` | String | Timestamp indicating when the product was created +`crosssell_products` | [ProductInterface] | An array of cross-sell products +`description` | ComplexTextValue | An object that contains detailed information about the product. The object can include simple HTML tags +`gift_message_available` | String | Indicates whether a gift message is available +`id` | Int | The ID number assigned to the product +`image` | [ProductImage](#ProductImage) | An object that contains the URL and label for the main image on the product page +`is_returnable` | String | Indicates whether the product can be returned. This attribute is defined in the `RmaGraphQl` module. +`manufacturer` | Int | A number representing the product's manufacturer +`media_gallery` | [[MediaGalleryInterface]](#MediaGalleryInterface) | An array of media gallery objects +`media_gallery_entries` | [MediaGalleryEntry] | Deprecated. Use `media_gallery` instead +`meta_description` | String | A brief overview of the product for search results listings, maximum 255 characters +`meta_keyword` | String | A comma-separated list of keywords that are visible only to search engines +`meta_title` | String | A string that is displayed in the title bar and tab of the browser and in search results lists +`name` | String | The product name. Customers use this name to identify the product. +`new_from_date` | String | The beginning date for new product listings, and determines if the product is featured as a new product +`new_to_date` | String | The end date for new product listings +`only_x_left_in_stock` | Float | The "Only X left Threshold" assigned to the product. This attribute is defined in the `InventoryGraphQl` module. +`options_container` | String | If the product has multiple options, determines where they appear on the product page +`price` | ProductPrices | Deprecated. Use `price_range` instead +`price_range` | [PriceRange!](#PriceRange) | A `PriceRange` object, indicating the range of prices for the product +`price_tiers` | [TierPrice] | An array of `TierPrice` objects +`product_links` | [ProductLinksInterface] | An array of [ProductLinks](#ProductLinks) objects +`related_products` | [ProductInterface] | An array of related products +`short_description` | ComplexTextValue | An object that contains a short description of the product. Its use depends on the store's theme. The object can include simple HTML tags +`sku` | String | A number or code assigned to a product to identify the product, options, price, and manufacturer +`small_image` | [ProductImage](#ProductImage) | An object that contains the URL and label for the small image used on catalog pages +`special_from_date` | String | The beginning date that a product has a special price +`special_price` | Float | The discounted price of the product +`special_to_date` | String | The end date that a product has a special price +`stock_status` | ProductStockStatus | The status of the stock. `ProductStockStatus` is an enumeration that can have the value of `IN_STOCK` or `OUT_OF_STOCK`. This attribute is defined in the `InventoryGraphQl` module. +`swatch_image` | String | The file name of a swatch image. This attribute is defined in the `SwatchesGraphQl` module. +`tax_class_id` | Int | An ID assigned to a tax class. This attribute is defined in the `TaxGraphQl` module. +`thumbnail` | [ProductImage](#ProductImage) | An object that contains the URL and label for the product's thumbnail image +`tier_price` | Float | Deprecated. Use `price_tiers` instead +`tier_prices` | [ProductTierPrices] | Deprecated. Use `price_tiers` instead +`type_id` | String | Deprecated. Use the GraphQL `__typename` meta attribute instead +`updated_at` | String | The timestamp indicating when the product was last updated +`upsell_products` | [ProductInterface] | An array of up-sell products +`url_key` | String | The part of the URL that identifies the product. This attribute is defined in the `CatalogUrlRewriteGraphQl` module +`url_path` | String | Deprecated. Use `canonical_url` instead +`url_suffix` | String | The part of the URL that is appended to the `url_key`, such as `.html`. This attribute is defined in the `CatalogUrlRewriteGraphQl` module +`url_rewrites` | [[UrlRewrite]](#urlRewriteObject) | A list of URL rewrites +`websites` | [[Website]](#websiteObject) | Deprecated. This attribute is not applicable for GraphQL + +### ProductPrices object {#ProductPrices} + +{:.bs-callout-info} +The `ProductPrices` object has been deprecated. Use the [`PriceRange`](#PriceRange) object instead. + +The `ProductPrices` object contains the regular price of an item, as well as its minimum and maximum prices. Only composite products, which include bundle, configurable, and grouped products, can contain a minimum and maximum price. + +Attribute | Data Type | Description +--- | --- | --- +`maximalPrice` | Price | Deprecated. Use `PriceRange.maximum_price` instead +`minimalPrice` | Price | Deprecated. Use `PriceRange.minimum_price` instead +`regularPrice` | Price | Deprecated. Use `PriceRange.maximum_price` or `PriceRange.minimum_price` instead + +### PriceRange object {#PriceRange} + +The `PriceRange` object defines the price range for a product. If a product only has a single price, the minimum and maximum price will be the same. + +Attribute | Data Type | Description +--- | --- | --- +`maximum_price` | ProductPrice | The highest possible final price for a product +`minimum_price` | ProductPrice | The lowest possible final price for a product + +### ProductPrice object {#ProductPrice} + +The `ProductPrice` object includes the regular price, final price, and the difference between those two prices. + +Attribute | Data Type | Description +--- | --- | --- +`discount` | ProductDiscount | The amount of the discount applied to the product. It represents the difference between the `final_price` and `regular_price` +`final_price`| Money! | The price of the product after applying discounts +`fixed_product_taxes` | [[FixedProductTax](#FixedProductTax)] | An array of fixed product taxes that either have been or can be applied to a product price +`regular_price` | Money! | The regular price of the product, without any applied discounts + +### ProductDiscount object {#ProductDiscount} + +The `ProductDiscount` object expresses the discount applied to a product as a fixed amount, such as $5, and as a percentage, such as 10%. The discount originates from special pricing or a catalog price rule. + +Attribute | Data Type | Description +--- | --- | --- +`amount_off` | Float | The actual value of the discount +`percent_off` | Float | The discount expressed as a percentage + +### FixedProductTax object {#FixedProductTax} + +Some tax jurisdictions have a fixed product tax (FPT) that must be applied to certain types of products. An example FPT is the Waste Electrical and Electronic Equipment (WEEE) tax, which is collected on some types of electronics to offset the cost of recycling. + +Attribute | Data Type | Description +--- | --- | --- +`amount` | Money | The amount of the fixed product tax +`label` | String | The label assigned to the fixed product tax to be displayed on the frontend + +### Price object {#Price} + +{:.bs-callout-info} +The `Price` object has been deprecated. Use the [`ProductPrice`](#ProductPrice) object instead. + +The `Price` object defines the price of a product as well as any tax-related adjustments. + +Attribute | Data Type | Description +--- | --- | --- +`amount` | Money | The price of the product and its currency code. See [Money object](#Money). +`adjustments` | [PriceAdjustment] | An array of [PriceAdjustment](#PriceAdjustment) objects. + +#### Money object {#Money} + +A `Money` object defines a monetary value, including a numeric value and a currency code. + +Attribute | Data Type | Description +--- | --- | --- +`currency` | CurrencyEnum | A three-letter currency code, such as `USD` or `EUR`. +`value` | Float | The price of the product + +#### PriceAdjustment array {#PriceAdjustment} + +{:.bs-callout-info} +The `PriceAdjustment` object has been deprecated. In cases where the value for the `code` attribute was `WEEE`, use `fixed_product_taxes.label` instead. If the value was `tax` or `weee_tax`, the taxes will be included or excluded as part of the price in the `ProductPrice` or `FixedProductTax` object, respectively. + +The `PriceAdjustment` object defines the amount of money to apply as an adjustment, the type of adjustment to apply, and whether the item is included or excluded from the adjustment. + +Attribute | Data Type | Description +--- | --- | --- +`amount` | Money | The amount of the price adjustment and its currency code. See [Money object](#Money). +`code` | PriceAdjustmentCodesEnum | One of `tax`, `weee`, or `weee_tax`. +`description` | PriceAdjustmentDescriptionEnum | Indicates whether the entity described by the code attribute is included or excluded from the adjustment. + +#### ProductLinks object {#ProductLinks} + +`ProductLinks` contains information about linked products, including the link type and product type of each item. + +Attribute | Type | Description +--- | --- | --- +`link_type` | String | One of `related`, `associated`, `upsell`, or `crosssell`. +`linked_product_sku` | String | The SKU of the linked product +`linked_product_type` | String | The type of linked product (`simple`, `virtual`, `bundle`, `downloadable`,`grouped`, `configurable`) +`position` | Int | The position within the list of product links +`sku` | String | The identifier of the linked product + +### MediaGalleryInterface {#MediaGalleryInterface} + +The `MediaGalleryInterface` contains basic information about a product image or video. + +Attribute | Type | Description +--- | --- | --- +`disabled` | Boolean | Indicates whether the media item is hidden from view +`label` | String | The label for the product image or video +`position` | Int | The media item's position after it has been sorted +`url` | String | The URL for the product image or video + +### ProductImage object {#ProductImage} + +`ProductImage` implements [`MediaGalleryInterface`](#MediaGalleryInterface), which contains information about an image's URL and label. + +### ProductVideo object {#ProductVideo} + +`ProductVideo` implements [`MediaGalleryInterface`](#MediaGalleryInterface) and contains information about a product video. + +Attribute | Type | Description +--- | --- | --- +`video_content` | ProductMediaGalleryEntriesVideoContent | Contains a [ProductMediaGalleryEntriesVideoContent](#ProductMediaGalleryEntriesVideoContent) object + +### MediaGalleryEntry object {#MediaGalleryEntry} + +`MediaGalleryEntry` defines characteristics about images and videos associated with a specific product. + +Attribute | Type | Description +--- | --- | --- +`content` | ProductMediaGalleryEntriesContent | Contains a [ProductMediaGalleryEntriesContent](#ProductMediaGalleryEntriesContent) object +`disabled` | Boolean | Whether the image is hidden from view +`file` | String | The path of the image on the server +`id` | Int | The identifier assigned to the object +`label` | String | The "alt" text displayed on the UI when the user points to the image +`media_type` | String | `image` or `video` +`position` | Int | The media item's position after it has been sorted +`types` | [String] | Array of image types. It can have the following values: `image`, `small_image`, `thumbnail` +`video_content` | ProductMediaGalleryEntriesVideoContent | Contains a [ProductMediaGalleryEntriesVideoContent](#ProductMediaGalleryEntriesVideoContent) object + +#### ProductMediaGalleryEntriesContent object {#ProductMediaGalleryEntriesContent} + +`ProductMediaGalleryEntriesContent` contains an image in base64 format and basic information about the image. + +Attribute | Type | Description +--- | --- | --- +`base64_encoded_data` | String | The image in base64 format +`name` | String | The file name of the image +`type` | String | The MIME type of the file, such as `image/png` + +#### ProductMediaGalleryEntriesVideoContent object {#ProductMediaGalleryEntriesVideoContent} + +`ProductMediaGalleryEntriesVideoContent` contains a link to a video file and basic information about the video. + +Attribute | Type | Description +--- | --- | --- +`media_type` | String | Must be `external-video` +`video_description` | String | A description of the video +`video_metadata` | String | Optional data about the video +`video_provider` | String | Optionally describes the video source +`video_title` | String | The title of the video +`video_url` | String | The URL to the video + +### ProductTierPrices object {#ProductTier} + +{:.bs-callout-info} +The `ProductTierPrices` object and all of its attributes have been deprecated. Use [`TierPrice`](#TierPrice) instead. + +The `ProductTierPrices` object defines a tier price, which is a quantity discount offered to a specific customer group. + +Attribute | Type | Description +--- | --- | --- +`customer_group_id` | Int | Deprecated. This attribute is not applicable for GraphQL +`percentage_value` | Float | Deprecated. Use `TierPrice.discount` instead +`qty` | Float | Deprecated. Use `TierPrice.quantity` instead +`value` | Float | Deprecated. Use `TierPrice.final_price` instead +`website_id` | Int | Deprecated. This attribute is not applicable for GraphQL + +### TierPrice object {#TierPrice} + +The `TierPrice` object defines a tier price, which is a price based on the quantity purchased. + +Attribute | Type | Description +--- | --- | --- +`discount` | ProductDiscount | The price discount applied to this tier +`final_price`| Money! | The price of the product at this tier +`quantity` | Float | The minimum number of items that must be purchased to qualify for this price tier + +### Website object {#websiteObject} + +{:.bs-callout-info} +The `Website` object has been deprecated because it is not applicable for GraphQL. + +Use the `Website` attributes to retrieve information about the website's configuration, which includes the website name, website code, and default group ID. The `Website` object is defined in the StoreGraphQl module. + +Attribute | Data Type | Description +--- | --- | --- +`code` | String | A code assigned to the website to identify it +`default_group_id` | String | The default group ID that the website has +`id` | Integer | The ID number assigned to the store +`is_default` | Boolean | Indicates whether this is the default website +`name` | String | The website name. Websites use this name to identify it easier +`sort_order` | Integer | The attribute to use for sorting websites + +### UrlRewrite object {#urlRewriteObject} + +The `products` query can request details about the `UrlRewrite` object. This object is defined in the UrlRewriteGraphQl module. + +Attribute | Type | Description +--- | --- | --- +`parameters` | [[`HttpQueryParameter`]](#HttpQueryParameter) | An array of target path parameters +`url` | String | The request URL + +### HTTPQueryParameter object {#HttpQueryParameter} + +The `HttpQueryParameter` object provides details about target path parameters. + +Attribute | Type | Description +--- | --- | --- +`name` | String | The parameter name, such as `id` +`value` | String | The value assigned to the parameter + +## PhysicalProductInterface {#PhysicalProductInterface} + +`PhysicalProductInterface`defines the weight of all tangible products. + +Attribute | Type | Description +--- | --- | --- +`weight` | Float | The weight of the item, in units defined by the store diff --git a/src/guides/v2.3/graphql/interfaces/simple-product.md b/src/guides/v2.3/graphql/interfaces/simple-product.md new file mode 100644 index 00000000000..145e0f9b1e0 --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/simple-product.md @@ -0,0 +1,109 @@ +--- +group: graphql +title: Simple product data types +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +The `SimpleProduct` data type implements the following interfaces: + +- [ProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html) +- [PhysicalProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html#PhysicalProductInterface) +- [CustomizableProductInterface]({{page.baseurl}}/graphql/interfaces/customizable-option-interface.html) + +All `SimpleProduct` attributes are defined in the implemented interfaces. + +## Sample Query + +The following [`products`]({{page.baseurl}}/graphql/queries/products.html) query returns information about simple product `24-MB01`, which is defined in the sample data. + +```graphql +{ + products( + filter: { + sku: { + eq: "24-MB01" + } + } + ) { + items { + sku + __typename + id + name + categories { + id + name + path + } + price_range { + minimum_price { + final_price { + currency + value + } + } + maximum_price { + final_price { + currency + value + } + } + } + stock_status + } + } +} +``` + +{% collapsible Response %} + +```json +{ + "data": { + "products": { + "items": [ + { + "sku": "24-MB01", + "__typename": "SimpleProduct", + "id": 1, + "name": "Joust Duffle Bag", + "categories": [ + { + "id": 3, + "name": "Gear", + "path": "1/2/3" + }, + { + "id": 4, + "name": "Bags", + "path": "1/2/3/4" + } + ], + "price_range": { + "minimum_price": { + "final_price": { + "currency": "USD", + "value": 34 + } + }, + "maximum_price": { + "final_price": { + "currency": "USD", + "value": 34 + } + } + }, + "stock_status": "IN_STOCK" + } + ] + } + } +} +``` + +{% endcollapsible %} + +## Related topics + +- [addSimpleProductsToCart mutation]({{page.baseurl}}/graphql/mutations/add-simple-products.html) diff --git a/src/guides/v2.3/graphql/interfaces/virtual-product.md b/src/guides/v2.3/graphql/interfaces/virtual-product.md new file mode 100644 index 00000000000..4f53d196f90 --- /dev/null +++ b/src/guides/v2.3/graphql/interfaces/virtual-product.md @@ -0,0 +1,103 @@ +--- +group: graphql +title: Virtual product data types +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +The `VirtualProduct` data type implements the following interfaces: + +- [ProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html) +- [CustomizableProductInterface]({{page.baseurl}}/graphql/interfaces/customizable-option-interface.html) + +All `VirtualProduct` attributes are defined in the implemented interfaces. + +## Sample Query + +The following [`products`]({{page.baseurl}}/graphql/queries/products.html) query returns information about a virtual product. + +```graphql +{ + products( + filter: { + sku: { + eq: "test-virtual-product" + } + } + ) { + items { + sku + __typename + id + name + categories { + id + name + path + } + price_range { + minimum_price { + final_price { + currency + value + } + } + maximum_price { + final_price { + currency + value + } + } + } + stock_status + } + } +} +``` + +{% collapsible Response %} + +```json +{ + "data": { + "products": { + "items": [ + { + "sku": "test-virtual-product", + "__typename": "VirtualProduct", + "id": 2047, + "name": "Test Virtual Product", + "categories": [ + { + "id": 37, + "name": "Sale", + "path": "1/2/37" + } + ], + "price_range": { + "minimum_price": { + "final_price": { + "currency": "USD", + "value": 123 + } + }, + "maximum_price": { + "final_price": { + "currency": "USD", + "value": 123 + } + } + }, + "stock_status": "IN_STOCK" + } + ] + } + } +} +``` + +{% endcollapsible %} + +## Related topics + +- [addVirtualProductsToCart mutation]({{page.baseurl}}/graphql/mutations/add-virtual-products.html) diff --git a/src/guides/v2.3/graphql/mutations/add-bundle-products.md b/src/guides/v2.3/graphql/mutations/add-bundle-products.md new file mode 100644 index 00000000000..7fd846ff375 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/add-bundle-products.md @@ -0,0 +1,256 @@ +--- +group: graphql +title: addBundleProductsToCart mutation +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +Use the `addBundleProductsToCart` mutation to add bundle products to a specific cart. + +## Syntax + +```graphql +mutation { + addBundleProductsToCart( + input: AddBundleProductsToCartInput + ) { + AddBundleProductsToCartOutput + } +} +``` + +## Example usage + +The following example uses a bundle product "Sprite Yoga Companion Kit" from Magento sample data. +The SKU of this product is: **24-WG080** + +This example adds one bundle product with following children to the specified shopping cart: + +- Sprite Stasis Ball 65 cm (x1) +- Sprite Foam Yoga Brick (x2) +- Sprite Yoga Strap 10 foot (x1) +- Sprite Foam Roller (x1) + +The `cart_id` used in this example was [generated]({{ page.baseurl }}/graphql/mutations/create-empty-cart.html) by creating an empty cart. + +**Request:** + +```graphql +mutation { + addBundleProductsToCart( + input: { + cart_id: "wARFaDnHva0tgzuforUYR4rvXincj5eu" + cart_items: [ + { + data: { + sku: "24-WG080" + quantity: 1 + } + bundle_options: [ + { + id: 1 + quantity: 1 + value: [ + "2" + ] + }, + { + id: 2 + quantity: 2 + value: [ + "4" + ] + }, + { + id: 3 + quantity: 1 + value: [ + "7" + ] + }, + { + id: 4 + quantity: 1 + value: [ + "8" + ] + } + ] + }, + ] + }) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addBundleProductsToCart": { + "cart": { + "items": [ + { + "id": "7", + "quantity": 1, + "product": { + "sku": "24-WG080" + }, + "bundle_options": [ + { + "id": 1, + "label": "Sprite Stasis Ball", + "type": "radio", + "values": [ + { + "id": 2, + "label": "Sprite Stasis Ball 65 cm", + "price": 27, + "quantity": 1 + } + ] + }, + { + "id": 2, + "label": "Sprite Foam Yoga Brick", + "type": "radio", + "values": [ + { + "id": 4, + "label": "Sprite Foam Yoga Brick", + "price": 5, + "quantity": 2 + } + ] + }, + { + "id": 3, + "label": "Sprite Yoga Strap", + "type": "radio", + "values": [ + { + "id": 7, + "label": "Sprite Yoga Strap 10 foot", + "price": 21, + "quantity": 1 + } + ] + }, + { + "id": 4, + "label": "Sprite Foam Roller", + "type": "radio", + "values": [ + { + "id": 8, + "label": "Sprite Foam Roller", + "price": 19, + "quantity": 1 + } + ] + } + ] + } + ] + } + } + } +} +``` + +## Input attributes + +The top-level `AddBundleProductsToCartInput` object is listed first. All interfaces and child objects are listed in alphabetical order. + +### AddBundleProductsToCartInput object + +The `AddBundleProductsToCartInput` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`cart_items` | [[BundleProductCartItemInput!]!](#bundleProductCartItemInput) | An array of bundle items to add to the cart + +### BundleProductCartItemInput object {#bundleProductCartItemInput} + +The `BundleProductCartItemInput` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`bundle_options` | [[BundleOptionInput!]!](#bundleOptionInput) | An object that contains an array of options of the bundle product with the chosen value and quantity of each option +`customizable_options` | [[CustomizableOptionInput!]](#customOptionInput) | An object that contains the ID and value of the product +`data` | [CartItemInput!](#cartItemInput) | An object that contains the quantity and SKU of the bundle product + +### BundleOptionInput object {#bundleOptionInput} + +The `BundleOptionInput` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`id` | Int! | ID of the option +`quantity` | Float! | The number of a specific child item to add to the cart +`value` | [String!]! | An array with the chosen value of the option + +### CartItemInput object {#cartItemInput} + +The `CartItemInput` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`quantity` | Float! | The number of items to add to the cart +`sku` | String! | The SKU of the product + +### CustomizableOptionInput object {#customOptionInput} + +The `CustomizableOptionInput` object must contain the following attributes: + +{% include graphql/customizable-option-input.md %} + +## Output attributes + +The `AddBundleProductsToCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Could not find a product with SKU "XXX"` | A simple product with the SKU specified in the `data.sku` argument does not exist. +`Required parameter "cart_id" is missing` | The `cart_id` argument is omitted or contains an empty value. + +## Related topics + +- [Bundle product data types]({{page.baseurl}}/graphql/interfaces/bundle-product.html) diff --git a/src/guides/v2.3/graphql/mutations/add-configurable-products.md b/src/guides/v2.3/graphql/mutations/add-configurable-products.md new file mode 100644 index 00000000000..056bd8f1406 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/add-configurable-products.md @@ -0,0 +1,158 @@ +--- +group: graphql +title: addConfigurableProductsToCart mutation +--- + +Use the `addConfigurableProductsToCart` mutation to add configurable products to a specific cart. + +## Syntax + +```graphql +mutation { + addConfigurableProductsToCart( + input: AddConfigurableProductsToCartInput + ) { + AddConfigurableProductsToCartOutput + } +} +``` + +## Example usage + +The following example adds two black Teton Pullover Hoodies size extra-small to the specified shopping cart. The `cart_id` used in this example was [generated]({{ page.baseurl }}/graphql/mutations/create-empty-cart.html) by creating an empty cart. + +**Request:** + +```graphql +mutation { + addConfigurableProductsToCart( + input: { + cart_id: "4JQaNVJokOpFxrykGVvYrjhiNv9qt31C" + cart_items: [ + { + parent_sku: "MH02" + data: { + quantity: 2 + sku: "MH02-XS-Black" + } + } + ] + } + ) { + cart { + items { + id + quantity + product { + name + sku + } + ... on ConfigurableCartItem { + configurable_options { + option_label + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addConfigurableProductsToCart": { + "cart": { + "items": [ + { + "id": "5", + "quantity": 2, + "product": { + "name": "Teton Pullover Hoodie", + "sku": "MH02" + }, + "configurable_options": [ + { + "option_label": "Color" + }, + { + "option_label": "Size" + } + ] + } + ] + } + } + } +} +``` + +## Input attributes + +### AddConfigurableProductsToCartInput object + +The `AddConfigurableProductsToCartInput` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`cart_items` | [[ConfigurableProductCartItemInput]](#configProdCartItemInput) | An array of configurable items to add to the cart + +### ConfigurableProductCartItemInput object {#configProdCartItemInput} + +The `ConfigurableProductCartItemInput` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`customizable_options` | [CustomizableOptionInput](#customOptionInput) | An object that contains the ID and value of the product +`data` | [CartItemInput!](#cartItemInput) | An object that contains the quantity and SKU of the configurable product +`parent_sku` | String | The SKU of the simple product's parent configurable product. If you do not specify this attribute, Magento treats the product being added to the cart as a simple product +`variant_sku` | String | Deprecated. Use `CartItemInput.sku` instead. The SKU of the simple product + +### CustomizableOptionInput object {#customOptionInput} + +The `CustomizableOptionInput` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`id` | Int | The ID of the customizable option +`value` | String | The value of the customizable option. For example, if color was the customizable option, a possible value could be `black` + +### CartItemInput object {#cartItemInput} + +The `CartItemInput` object must contain the following attributes: + +{% include graphql/cart-item-input.md %} + +## Output attributes + +The `AddConfigurableProductsToCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Could not add the product with SKU configurable to the shopping cart: The product that was requested doesn't exist. Verify the product and try again.` | The simple product with the SKU specified in the `data`.`sku` attribute does not exist. +`Could not find a product with SKU "XXX"` | The configurable product with SKU specified in the `parent_sku` argument does not exist. +`Could not find specified product.` | The simple product specified in the `data`.`sku` argument is not assigned to the configurable product provided in the `parent_sku` attribute. +`Required parameter "cart_id" is missing` | The `cart_id` argument was omitted or contains an empty value. +`Required parameter "cart_items" is missing` | The `cart_items` argument was omitted or contains an empty array. +`Required parameter "email" is missing` | The `email` argument was omitted or contains an empty value. +`The requested qty is not available` | The requested quantity specified `data`.`quantity` is not available. + +## Related topics + +- [Configurable product data types]({{page.baseurl}}/graphql/interfaces/configurable-product.html) diff --git a/src/guides/v2.3/graphql/mutations/add-downloadable-products.md b/src/guides/v2.3/graphql/mutations/add-downloadable-products.md new file mode 100644 index 00000000000..7247b746826 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/add-downloadable-products.md @@ -0,0 +1,265 @@ +--- +group: graphql +title: addDownloadableProductsToCart mutation +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +A downloadable product can be anything that you can deliver as a file, such as an eBook, music, video, software application, or an update. To add a downloadable product to a cart, you must provide the cart ID, the SKU, and the quantity. In some cases, you must include the IDs for downloadable product links. You can also optionally specify customizable options. + +## Syntax + +```graphql +mutation { + addDownloadableProductsToCart( + input: AddDownloadableProductsToCartInput + ) { + AddDownloadableProductsToCartOutput + } +} +``` + +## Example usage + +The following examples show how to add a downloadable product to a shopping cart , depending on whether the **Links can be purchased separately** option is selected on the **Downloadable Information** section of the product page. + +### Add a downloadable product to a cart with `Links can be purchased separately` enabled + +The following example shows how to add a downloadable product in which the **Links can be purchased separately** option is enabled. The payload includes custom downloadable links `Episode 2` and `Episode 3`. + +**Request:** + +```graphql +mutation { + addDownloadableProductsToCart( + input: { + cart_id: "gMV2BFQuNGiQmTnepQlMGko7Xc4P3X1w" + cart_items: { + data: { + sku: "240-LV09" + quantity: 1 + } + downloadable_product_links: [ + { + link_id: 7 # Episode 2 + } + { + link_id: 8 # Episode 3 + } + ] + } + } + ) { + cart { + items { + product { + sku + } + quantity + ... on DownloadableCartItem { + links { + title + price + } + samples { + title + sample_url + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addDownloadableProductsToCart": { + "cart": { + "items": [ + { + "product": { + "sku": "240-LV09" + }, + "quantity": 1, + "links": [ + { + "title": "Episode 2", + "price": 9 + }, + { + "title": "Episode 3", + "price": 9 + } + ], + "samples": [ + { + "title": "Trailer #1", + "sample_url": "https:///downloadable/download/sample/sample_id/16/" + }, + { + "title": "Trailer #2", + "sample_url": "https:///downloadable/download/sample/sample_id/17/" + }, + { + "title": "Trailer #3", + "sample_url": "https:///downloadable/download/sample/sample_id/18/" + } + ] + } + ] + } + } + } +} +``` + +### Add a downloadable product to a cart with disabled `Links can be purchased separately` + +The following example shows how to add a downloadable product in which the **Links can be purchased separately** option is disabled. All downloadable links are added to the cart automatically. + +**Request:** + +```graphql +mutation { + addDownloadableProductsToCart( + input: { + cart_id: "gMV2BFQuNGiQmTnepQlMGko7Xc4P3X1w" + cart_items: { + data: { + sku: "240-LV07" + quantity: 1 + } + } + } + ) { + cart { + items { + product { + sku + } + quantity + ... on DownloadableCartItem { + links { + title + price + } + samples { + title + sample_url + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addDownloadableProductsToCart": { + "cart": { + "items": [ + { + "product": { + "sku": "240-LV07" + }, + "quantity": 2, + "links": [ + { + "title": "Solo Power Circuit", + "price": 14 + } + ], + "samples": [ + { + "title": "Trailer #1", + "sample_url": "https:///downloadable/download/sample/sample_id/10/" + }, + { + "title": "Trailer #2", + "sample_url": "https:///downloadable/download/sample/sample_id/11/" + }, + { + "title": "Trailer #3", + "sample_url": "https:///downloadable/download/sample/sample_id/12/" + } + ] + } + ] + } + } + } +} +``` + +## Input attributes + +The top-level `AddDownloadableProductsToCartInput` object is listed first. All child objects are listed in alphabetical order. + +### AddDownloadableProductsToCartInput object {#AddDownloadableProductsToCartInput} + +The `AddDownloadableProductsToCartInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`cart_items` | [[DownloadableProductCartItemInput!]!](#DownloadableProductCartItemInput) | Contains the cart item IDs and quantity of each item + +### CartItemInput object {#CartItemInputVirtual} + +The `CartItemInput` object must contain the following attributes: + +{% include graphql/cart-item-input.md %} + +### CustomizableOptionInput object {#CustomizableOptionInputVirtual} + +The `CustomizableOptionInput` object must contain the following attributes: + +{% include graphql/customizable-option-input.md %} + +### DownloadableProductCartItemInput object {#DownloadableProductCartItemInput} + +The `DownloadableProductCartItemInput` object can contain the following attribute: + +Attribute | Data Type | Description +--- | --- | --- +`customizable_options` |[[CustomizableOptionInput!]](#CustomizableOptionInputVirtual) | An array that defines customizable options for the product +`data` | [CartItemInput!](#CartItemInputVirtual) | Required. An object containing the `sku` and `quantity` of the product +`downloadable_product_links` | [[DownloadableProductLinksInput!]](#DownloadableProductLinksInput) | An object containing the `link_id` of the downloadable product link + +### DownloadableProductLinksInput object {#DownloadableProductLinksInput} + +If specified, the `DownloadableProductLinksInput` object must contain the following attribute. + +Attribute | Data Type | Description +--- | --- | --- +`link_id` | Int! | A unique ID (`downloadable_link`.`link_id`) of the downloadable product link + +## Output attributes + +The `AddDownloadableProductsToCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Could not find a product with SKU "YYY"` | A product with the SKU specified in the `data`.`sku` argument does not exist. +`Required parameter "cart_id" is missing` | The mutation does not contain a `cart_id` argument. +`Required parameter "cart_items" is missing` | The `cart_items` argument is empty or is not of type `array`. +`Please specify product link(s).` | You tried to add a downloadable product in which the `Links can be purchased separately` option is enabled, but you did not specify individual product links. diff --git a/src/guides/v2.3/graphql/mutations/add-simple-products.md b/src/guides/v2.3/graphql/mutations/add-simple-products.md new file mode 100644 index 00000000000..044dea34e0f --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/add-simple-products.md @@ -0,0 +1,292 @@ +--- +group: graphql +title: addSimpleProductsToCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-add-simple-products.html +--- + +The `addSimpleProductsToCart` mutation allows you to add any number of simple and group products to the cart at the same time. + +Simple products are physical products that do not have variations, such as color, size, or price. Group products are a set of simple standalone products that are assigned a unique SKU and are presented as a group. Each product in the group is purchased as a separate item. + +To add a simple or grouped product to a cart, you must provide the cart ID, the SKU, and the quantity. You can also optionally provide customizable options. + +## Syntax + +```graphql +mutation { + addSimpleProductsToCart( + input: AddSimpleProductsToCartInput + ) { + AddSimpleProductsToCartOutput + } +} +``` + +## Example usage + +These examples show the minimal payload and a payload that includes customizable options. + +### Add a simple product to a cart + +The following example adds a simple product to a cart. The response contains the entire contents of the customer's cart. + +**Request:** + +```graphql +mutation { + addSimpleProductsToCart( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + cart_items: [ + { + data: { + quantity: 1 + sku: "24-MB04" + } + } + ] + } + ) { + cart { + items { + id + product { + name + sku + } + quantity + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addSimpleProductsToCart": { + "cart": { + "items": [ + { + "id": "13", + "product": { + "name": "Strive Shoulder Pack", + "sku": "24-MB04" + }, + "quantity": 1 + } + ] + } + } + } +} +``` + +### Add a simple product with customizable options to a cart + +If a product has a customizable option, you can specify the option's value in the `addSimpleProductsToCart` request. + +**Request:** + +```graphql +mutation { + addSimpleProductsToCart (input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG", + cart_items: [ + { + data: { + sku: "simple" + quantity: 1 + }, + customizable_options: [ + { + id: 121 + value_string: "field value" + } + ] + } + ] + }) { + cart { + items { + product { + name + } + quantity + ... on SimpleCartItem { + customizable_options { + label + values { + value + } + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addSimpleProductsToCart": { + "cart": { + "items": [ + { + "product": { + "name": "simple" + }, + "quantity": 1, + "customizable_options": [ + { + "label": "Field Option", + "values": [ + { + "value": "field value" + } + ] + } + ] + } + ] + } + } + } +} +``` +### Add a grouped product to a cart + +The following example adds a grouped product (`Workout-Kit`) to a cart. The grouped product contains three simple products. + +**Request:** + +```graphql +mutation { + addSimpleProductsToCart( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + cart_items: [ + { + data: { + quantity: 1 + sku: "Workout-Kit" + } + } + ] + } + ) { + cart { + items { + id + product { + name + sku + } + quantity + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addSimpleProductsToCart": { + "cart": { + "items": [ + { + "id": "5", + "product": { + "name": "Go-Get'r Pushup Grips", + "sku": "24-UG05" + }, + "quantity": 1 + }, + { + "id": "6", + "product": { + "name": "Dual Handle Cardio Ball", + "sku": "24-UG07" + }, + "quantity": 1 + }, + { + "id": "7", + "product": { + "name": "Harmony Lumaflex™ Strength Band Kit ", + "sku": "24-UG03" + }, + "quantity": 1 + } + ] + } + } + } +} +``` + +## Input attributes + +The top-level `AddSimpleProductsToCartInput` object is listed first. All child objects are listed in alphabetical order. + +### AddSimpleProductsToCartInput object {#AddSimpleProductsToCartInput} + +The `AddSimpleProductsToCartInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`cart_items` | [SimpleProductCartItemInput!](#SimpleProductCartItemInput) | Contains the cart item IDs and quantity of each item + +### CartItemInput object {#CartItemInputSimple} + +The `CartItemInput` object must contain the following attributes: + +{% include graphql/cart-item-input.md %} + +### CustomizableOptionInput object {#CustomizableOptionInputSimple} + +The `CustomizableOptionInput` object must contain the following attributes: + +{% include graphql/customizable-option-input.md %} + +### SimpleProductCartItemInput object {#SimpleProductCartItemInput} + +The `SimpleProductCartItemInput` object must contain the following attributes: + +`customizable_options` |[[CustomizableOptionInputSimple]](#CustomizableOptionInputSimple) | An array that defines customizable options for the product +`data` | [CartItemInput!](#CartItemInputSimple) | An object containing the `sku` and `quantity` of the product. + +## Output attributes + +The `AddSimpleProductsToCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Could not find a product with SKU "YYY"` | A simple product with the SKU specified in the `data`.`sku` argument does not exist. +`Required parameter "cart_id" is missing` | The `cart_id` argument was omitted or contains an empty value. +`Required parameter "cart_items" is missing` | The `cart_items` argument was omitted or contains an empty value. +`The current user cannot perform operations on cart XXX` | An unauthorized user (guest) tried to add the product into a customer's cart, or an authorized user (customer) tried to add the product into the cart of another customer. +`The product's required option(s) weren't entered. Make sure the options are entered and try again.` | A simple product has customizable options that were not specified in the mutation, but are required for adding the product into the cart. diff --git a/src/guides/v2.3/graphql/mutations/add-virtual-products.md b/src/guides/v2.3/graphql/mutations/add-virtual-products.md new file mode 100644 index 00000000000..1d9f074efc3 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/add-virtual-products.md @@ -0,0 +1,147 @@ +--- +group: graphql +title: addVirtualProductsToCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-add-virtual-products.html +--- + +A virtual product represents a saleable item that is not physical, such as a membership, service, warranty, or subscription. Virtual products do not need to be shipped or downloaded, nor do they require stock management. + +The `addVirtualProductsToCart` mutation allows you to add multiple virtual products to the cart at the same time, but you cannot add other product types with this mutation. To add a virtual product to a cart, you must provide the cart ID, the SKU, and the quantity. You can also optionally provide customizable options. + +## Syntax + +```graphql +mutation { + addVirtualProductsToCart( + input: AddVirtualProductsToCartInput + ) { + AddVirtualProductsToCartOutput + } +} +``` + +## Example usage + +The Luma sample data does not include any virtual products. The following example requires that you create a virtual product with the `sku` value of `Membership-Gold` with a price of $49.99. + +**Request:** + +```graphql +mutation { + addVirtualProductsToCart( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG", + cart_items: [ + { + data: { + quantity: 1 + sku: "Membership-Gold" + } + } + ] + } + ) { + cart { + items { + product { + name + } + quantity + } + prices { + grand_total { + value + currency + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addVirtualProductsToCart": { + "cart": { + "items": [ + { + "product": { + "name": "Gold Membership" + }, + "quantity": 1 + } + ], + "prices": { + "grand_total": { + "value": 49.99, + "currency": "USD" + } + } + } + } + } +} +``` + +## Input attributes + +The top-level `AddVirtualProductsToCartInput` object is listed first. All child objects are listed in alphabetical order. + +### AddVirtualProductsToCartInput object {#AddVirtualProductsToCartInput} + +The `AddVirtualProductsToCartInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`cart_items` | [VirtualProductCartItemInput!](#VirtualProductCartItemInput) | Contains the cart item IDs and quantity of each item + +### CartItemInput object {#CartItemInputVirtual} + +The `CartItemInput` object must contain the following attributes: + +{% include graphql/cart-item-input.md %} + +### CustomizableOptionInput object {#CustomizableOptionInputVirtual} + +The `CustomizableOptionInput` object must contain the following attributes: + +{% include graphql/customizable-option-input.md %} + +### VirtualProductCartItemInput object {#VirtualProductCartItemInput} + +The `VirtualProductCartItemInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`customizable_options` |[[CustomizableOptionInput]](#CustomizableOptionInputVirtual) | An array that defines customizable options for the product +`data` | [CartItemInput!](#CartItemInputVirtual) | An object containing the `sku` and `quantity` of the product + +## Output attributes + +The `AddVirtualProductsToCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Could not find a product with SKU "YYY"` | A virtual product with the SKU specified in the `data`.`sku` argument does not exist. +`Required parameter "cart_id" is missing` | The `cart_id` argument was omitted or contains an empty value. +`Required parameter "cart_items" is missing` | The `cart_items` argument was omitted or contains an empty value. +`The current user cannot perform operations on cart XXX` | An unauthorized user (guest) tried to add the product into a customer's cart, or an authorized user (customer) tried to add the product into the cart of another customer. +`The product's required option(s) weren't entered. Make sure the options are entered and try again.` | A virtual product has customizable options that were not specified in the mutation, but are required for adding the product into the cart. diff --git a/src/guides/v2.3/graphql/mutations/apply-coupon.md b/src/guides/v2.3/graphql/mutations/apply-coupon.md new file mode 100644 index 00000000000..eb2f2ff662c --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/apply-coupon.md @@ -0,0 +1,135 @@ +--- +group: graphql +title: applyCouponToCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-apply-coupon.html +--- + +The `applyCouponToCart` mutation applies a pre-defined coupon code to the specified cart. Valid coupon codes are defined in cart price rules. + +## Syntax + +```graphql +mutation { + applyCouponToCart( + input: ApplyCouponToCartInput + ) { + ApplyCouponToCartOutput + } +} +``` + +## Example usage + +The following example applies the coupon code `H2O` to the cart. For this coupon to be valid, the Affirm Water Bottle (`sku`: 24-UG06) must be in the cart. + +**Request:** + +```graphql +mutation { + applyCouponToCart( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG", + coupon_code: "H20" + } + ) { + cart { + items { + product { + name + } + quantity + } + applied_coupons { + code + } + prices { + grand_total{ + value + currency + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "applyCouponToCart": { + "cart": { + "items": [ + { + "product": { + "name": "Gold Membership" + }, + "quantity": 2 + }, + { + "product": { + "name": "Strive Shoulder Pack" + }, + "quantity": 1 + }, + { + "product": { + "name": "Affirm Water Bottle " + }, + "quantity": 1 + } + ], + "applied_coupons": { + "code": "H20" + }, + "prices": { + "grand_total": { + "value": 134.08, + "currency": "USD" + } + } + } + } + } +} +``` + +## Input attributes + +The `applyCouponToCart` mutation requires the `cart_id` and `coupon_code`. + +### ApplyCouponToCartInput object {#ApplyCouponToCartInput} + +The `ApplyCouponToCartInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`coupon_code` | String! | A valid coupon code + +## Output attributes + +The `ApplyCouponToCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`A coupon is already applied to the cart. Please remove it to apply another` | The value specified in the `coupon_code` argument has already applied to cart. Use [removeCouponFromCart]({{page.baseurl}}/graphql/mutations/remove-coupon.html) to remove the current coupon and to apply another. +`Cart does not contain products.` | The coupon cannot be applied to an empty cart. +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Required parameter "coupon_code" is missing` | The required `coupon_code` argument contains an empty value. +`The coupon code isn't valid. Verify the code and try again.` | The entered coupon code is not applicable. Check the existing shopping cart price rules for details. +`The current user cannot perform operations on cart XXX` | An unauthorized user (guest) tried to add the product into a customer's cart, or an authorized user (customer) tried to add the product into the cart of another customer. diff --git a/src/guides/v2.3/graphql/mutations/apply-giftcard.md b/src/guides/v2.3/graphql/mutations/apply-giftcard.md new file mode 100644 index 00000000000..34b56fe0767 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/apply-giftcard.md @@ -0,0 +1,107 @@ +--- +group: graphql +title: applyGiftCardToCart mutation +ee_only: True +redirect from: + - /guides/v2.3/graphql/reference/quote-apply-giftcard.html +--- + +The `applyGiftCardToCart` mutation applies a pre-defined gift card code to the specified cart. + +## Syntax + +```graphql +mutation { + applyGiftCardToCart( + input: ApplyGiftCardToCartInput + ) { + ApplyGiftCardToCartOutput + } +} +``` + +## Example usage + +The following example adds a gift card with the code `0330CEIVTLB4` to the cart. The gift card has a value of $20. + +**Request:** + +```graphql +mutation { + applyGiftCardToCart( + input: { + cart_id: "lY8PnKhlHBGc4WS5v0Y3dWjxiA5PvvgY" + gift_card_code: "0330CEIVTLB4" + } + ) { + cart { + applied_gift_cards { + applied_balance { + value + currency + } + code + current_balance { + value + currency + } + expiration_date + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "applyGiftCardToCart": { + "cart": { + "applied_gift_cards": [ + { + "applied_balance": { + "value": 20, + "currency": "USD" + }, + "code": "0330CEIVTLB4", + "current_balance": { + "value": 20, + "currency": "USD" + }, + "expiration_date": null + } + ] + } + } + } +} +``` + +## Input attributes + +The `applyGiftCardToCart` mutation requires the `cart_id` and `gift_card_code`. + +### ApplyGiftCardToCartInput object {#ApplyGiftCardToCartInput} + +The `ApplyGiftCardToCartInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`gift_card_code` | String! | The gift card code + +## Output attributes + +The `ApplyGiftCardToCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + + {% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. \ No newline at end of file diff --git a/src/guides/v2.3/graphql/mutations/apply-store-credit.md b/src/guides/v2.3/graphql/mutations/apply-store-credit.md new file mode 100644 index 00000000000..80aa64fc467 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/apply-store-credit.md @@ -0,0 +1,120 @@ +--- +group: graphql +title: applyStoreCreditToCart mutation +ee_only: true +--- + +The `applyStoreCreditToCart` mutation applies store credit to the specified cart. Store credit must be enabled on the store to run this mutation. + +Store credit is an amount that the merchant applies to a customer account as a result of a refund or similar transaction. If gift cards are enabled for a store, then a customer receives store credit when redeeming a gift card. No matter how the customer obtains store credit, these funds can be used to pay for purchases. + +The amount returned in the `current_balance` indicates how much store credit at the time you run the `applyStoreCreditToCart` mutation. This amount is not decreased until you place the order. + +{:.bs-callout-info} +If the amount of available store credit equals or exceeds the grand total of the quote, set the payment method to `free` in the `setPaymentMethodOnCart` mutation. + +## Syntax + +```graphql +mutation { + applyStoreCreditToCart( + input: ApplyStoreCreditToCartInput + ) { + ApplyStoreCreditToCartOutput + } +} +``` + +## Example usage + +In the following example, the customer starts with $10 of store credit. The subtotal of the items in the cart before applying the store credit plus shipping and tax is $34.64. The grand total on the cart after applying the store credit is $24.64. + +**Request:** + +```graphql +mutation { + applyStoreCreditToCart( + input: { + cart_id: "4HHaKzxpKM2ZwD0IcheRfcPNBWS3OvRM" + } + ) { + cart { + applied_store_credit { + applied_balance { + currency + value + } + current_balance { + currency + value + } + } + prices { + grand_total { + currency + value + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "applyStoreCreditToCart": { + "cart": { + "applied_store_credit": { + "applied_balance": { + "currency": "USD", + "value": 34.64 + }, + "current_balance": { + "currency": "USD", + "value": 10 + } + }, + "prices": { + "grand_total": { + "currency": "USD", + "value": 24.64 + } + } + } + } + } +} +``` + +## Input attributes + +The `ApplyStoreCreditToCartInput` object must contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer’s cart + +## Output attributes + +The `ApplyStoreCreditToCartOutput` object returns the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID \"xxxxx\"` | The ID provided in the `cart_id` field is invalid or the cart does not exist for the customer. +`The cart isn't active` | The cart with the given cart ID is unavailable, because the items have been purchased and the cart ID becomes inactive. +`Field ApplyStoreCreditToCartInput.cart_id of required type String! was not provided` | The value specified in the `ApplyStoreCreditToCartInput.cart_id` argument is empty. diff --git a/src/guides/v2.3/graphql/mutations/change-customer-password.md b/src/guides/v2.3/graphql/mutations/change-customer-password.md new file mode 100644 index 00000000000..9c1bb0d9058 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/change-customer-password.md @@ -0,0 +1,83 @@ +--- +group: graphql +title: changeCustomerPassword mutation +--- + +Use the `changeCustomerPassword` mutation to change the password for the logged-in customer. + +To return or modify information about a customer, we recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +```graphql +mutation { + changeCustomerPassword( + currentPassword: String! + newPassword: String! + ) { + Customer + } +} +``` + +## Example usage + +The following call updates the customer's password. + +**Request:** + +```graphql +mutation { + changeCustomerPassword( + currentPassword: "roni_cost3@example.com" + newPassword: "roni_cost4@example.com" + ) { + id + email + } +} +``` + +**Response:** + +```json +{ + "data": { + "changeCustomerPassword": { + "id": 1, + "email": "roni_cost@example.com" + } + } +} +``` + +## Input attributes + +The `changeCustomerPassword` mutation requires the following inputs: + +Attribute | Data Type | Description +--- | --- | --- +`currentPassword` | String | The customer's current password +`newPassword` | String | The customer's new password + +## Output attributes + +The `changeCustomerPassword` mutation returns the `customer` object. + +{% include graphql/customer-output.md %} + +## Errors + +Error | Description +--- | --- +`The current customer isn't authorized.` | The customer's token does not exist in the `oauth_token` table. +`Invalid login or password.` | The password specified in the `currentPassword` argument is not valid. +`Specify the "currentPassword" value.` | The password specified in the `currentPassword` argument is empty. +`Specify the "newPassword" value.` | The password specified in the `newPassword` argument is empty. +`The account is locked.` | The customer's password cannot be changed because the account is locked. + +## Related topics + +* [customer query]({{page.baseurl}}/graphql/queries/customer.html) +* [createCustomer mutation]({{page.baseurl}}/graphql/mutations/create-customer.html) +* [updateCustomer mutation]({{page.baseurl}}/graphql/mutations/update-customer.html) diff --git a/src/guides/v2.3/graphql/mutations/create-braintree-client-token.md b/src/guides/v2.3/graphql/mutations/create-braintree-client-token.md new file mode 100644 index 00000000000..07b6fc38407 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/create-braintree-client-token.md @@ -0,0 +1,44 @@ +--- +group: graphql +title: createBraintreeClientToken mutation +contributor_name: Something Digital +contributor_link: https://www.somethingdigital.com/ +--- + +The `createBraintreeClientToken` mutation creates the client token for Braintree Javascript SDK initialization. + +## Syntax + +```graphql +mutation { + createBraintreeClientToken { + String + } +} +``` + +## Example usage + +**Request:** + +```graphql +mutation { + createBraintreeClientToken +} +``` + +**Response:** + +```json +{ + "data": { + "createBraintreeClientToken": "4JQaNVJokOpFxrykGVvYrjhiNv9qt31C" + } +} +``` + +## Errors + +Error | Description +--- | --- +`The Braintree payment method is not active.` | The [Braintree](https://docs.magento.com/m2/ee/user_guide/payment/braintree.html) payment method is disabled in admin. diff --git a/src/guides/v2.3/graphql/mutations/create-customer-address.md b/src/guides/v2.3/graphql/mutations/create-customer-address.md new file mode 100644 index 00000000000..062083bea04 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/create-customer-address.md @@ -0,0 +1,118 @@ +--- +group: graphql +title: createCustomerAddress mutation +--- + +Use the `createCustomerAddress` mutation to create the customer's address. + +To return or modify information about a customer, we recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +```graphql +mutation { + createCustomerAddress( + input: CustomerAddressInput! + ) { + CustomerAddress + } +} +``` + +## Example usage + +The following call creates an address for the specified customer. + +**Request:** + +```graphql +mutation { + createCustomerAddress(input: { + region: { + region: "Arizona" + region_code: "AZ" + } + country_code: US + street: ["123 Main Street"] + telephone: "7777777777" + postcode: "77777" + city: "Phoenix" + firstname: "Bob" + lastname: "Loblaw" + default_shipping: true + default_billing: false + }) { + id + region { + region + region_code + } + country_code + street + telephone + postcode + city + default_shipping + default_billing + } +} +``` + +**Response:** + +```json +{ + "data": { + "createCustomerAddress": { + "id": 4, + "region": { + "region": "Arizona", + "region_code": "AZ" + }, + "country_code": "US", + "street": [ + "123 Main Street" + ], + "telephone": "7777777777", + "postcode": "77777", + "city": "Phoenix", + "default_shipping": true, + "default_billing": false + } + } +} +``` + +## Input attributes + +Attribute | Data Type | Description +--- | --- | --- +`id` | Int | The ID assigned to the address object +`CustomerAddressInput` | [CustomerAddress](#customerAddressInput) | An array containing the customer’s shipping and billing addresses + +{% include graphql/customer-address-input.md %} + +## Output attributes + +The `createCustomerAddress` mutation returns the following attributes: + +{% include graphql/customer-address-output.md %} + +## Errors + +Error | Description +--- | --- +`Expected type CustomerAddressInput!, found "".` | The `input` attribute contains an empty value. +`"input" value should be specified` | The `input` argument is specified but is empty. +`Required parameters are missing: firstname` | The `input.firstname` argument was omitted or contains an empty value. +`"Street Address" cannot contain more than 2 lines.` | The `input.street` argument contains array with more than two elements. +`Syntax Error: Expected Name, found )` | The `input` argument was omitted. +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. + +## Related topics + +* [customer query]({{page.baseurl}}/graphql/queries/customer.html) +* [createCustomer mutation]({{page.baseurl}}/graphql/mutations/create-customer.html) +* [updateCustomer mutation]({{page.baseurl}}/graphql/mutations/update-customer.html) +* [updateCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/update-customer-address.html) +* [deleteCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/delete-customer-address.html) diff --git a/src/guides/v2.3/graphql/mutations/create-customer.md b/src/guides/v2.3/graphql/mutations/create-customer.md new file mode 100644 index 00000000000..4ce3efa8e36 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/create-customer.md @@ -0,0 +1,94 @@ +--- +group: graphql +title: createCustomer mutation +--- + +Use the `createCustomer` mutation to create a new customer. + +To return or modify information about a customer, we recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +```graphql +mutation { + createCustomer( + input: CustomerInput! + ) { + CustomerOutput + } +} +``` + +## Example usage + +The following call creates a new customer. + +**Request:** + +```graphql +mutation { + createCustomer( + input: { + firstname: "Bob" + lastname: "Loblaw" + email: "bobloblaw@example.com" + password: "b0bl0bl@w" + is_subscribed: true + } + ) { + customer { + firstname + lastname + email + is_subscribed + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "createCustomer": { + "customer": { + "firstname": "Bob", + "lastname": "Loblaw", + "email": "bobloblaw@example.com", + "is_subscribed": true + } + } + } +} +``` + +## Input attributes + +The following table lists the attributes you can use as input for the `createCustomer` mutation. The [Customer attributes]({{page.baseurl}}/graphql/queries/customer.html#customerAttributes) table lists the attributes Magento returns. + +{% include graphql/create-customer.md %} + +## Output attributes + +The `createCustomer` mutation returns the `CustomerOutput` object. + +{% include graphql/customer-output.md %} + +## Errors + +Error | Description +--- | --- +`A customer with the same email address already exists in an associated website.` | The email provided in the `input`.`email` argument belongs to an existing customer. +`"Email" is not a valid email address.` | The value provided in the `input`.`email` argument has an invalid format. +`Field CustomerInput.email of required type String! was not provided` | The `input`.`email` argument was omitted. +`Field "xxx" is not defined by type CustomerInput.` | The `input`.`xxx` argument is undefined. +`Required parameters are missing: First Name` | The `input`.`firstname` argument was omitted or contains an empty value. + +## Related topics + +* [customer query]({{page.baseurl}}/graphql/queries/customer.html) +* [updateCustomer mutation]({{page.baseurl}}/graphql/mutations/update-customer.html) +* [createCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/create-customer-address.html) +* [updateCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/update-customer-address.html) +* [deleteCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/delete-customer-address.html) diff --git a/src/guides/v2.3/graphql/mutations/create-empty-cart.md b/src/guides/v2.3/graphql/mutations/create-empty-cart.md new file mode 100644 index 00000000000..4f8bb49c3eb --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/create-empty-cart.md @@ -0,0 +1,89 @@ +--- +group: graphql +title: createEmptyCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-create-cart.html +--- + +The `createEmptyCart` mutation creates an empty shopping cart for a guest or logged in customer. You can allow the system to generate a cart ID, or assign a specific ID. + +If you are creating a cart for a logged in customer, you must include the customer's authorization token in the header of the request. + +## Syntax + +```graphql +mutation { + createEmptyCart { + String + } +} +``` + +## Example usage + +### Create a cart with a randomly-generated cart ID + +**Request:** + +```graphql +mutation { + createEmptyCart +} +``` + +**Response:** + +The response is the cart ID, which is sometimes called the quote ID. The remaining examples in this topic will use this cart ID. + +```json +{ + "data": { + "createEmptyCart": "4JQaNVJokOpFxrykGVvYrjhiNv9qt31C" + } +} +``` + +### Create an empty cart with an assigned cart ID + +You can also create an empty cart with a specified `cart_id`. + +**Request:** + +```graphql +mutation { + createEmptyCart( + input: { + cart_id: "x2345678901234567890123456789012" + } + ) +} +``` + +**Response:** + +The mutation returns the same `cart_id`. + +```json +{ + "data": { + "createEmptyCart": "x2345678901234567890123456789012" + } +} +``` + +## Input attributes + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String | An optional 32-character string + +## Output attributes + +The `createEmptyCart` mutation returns the cart ID. + +## Errors + +Error | Description +--- | --- +`Cart with ID "XXX" already exists.` | The specified cart ID was previously used to create a cart. +`Cart ID length should to be 32 symbols.` | The cart ID is not the required length. diff --git a/src/guides/v2.3/graphql/mutations/create-payflow-pro-token.md b/src/guides/v2.3/graphql/mutations/create-payflow-pro-token.md new file mode 100644 index 00000000000..db224e555b5 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/create-payflow-pro-token.md @@ -0,0 +1,116 @@ +--- +group: graphql +title: createPayflowProToken mutation +--- + +The `createPayflowProToken` mutation initiates a Payflow Pro transaction and receives a token. The payload must contain the redirect URLs to use when the transaction is successful, when the customer cancels the transaction, or when an error occurs. + +See [Paypal Payflow Pro payment method]({{page.baseurl}}/graphql/payment-methods/payflow-pro.html) for detailed information about the workflow of PayPal Payflow Pro transactions. + +## Syntax + +```graphql +mutation { + createPayflowProToken( + input: PayflowProTokenInput + ) { + CreatePayflowProTokenOutput + } +} +``` + +## Example usage + +The following example requests a token in a Payflow Pro transaction. + +**Request:** + +```graphql +mutation { + createPayflowProToken( + input: { + cart_id: "Po1WkfK7d3vZE0qga610NwJIbxgqllpt" + urls: { + return_url: "paypal/action/return.html" + cancel_url: "paypal/action/cancel.html" + error_url: "paypal/action/error.html" + } + } + ) { + response_message + result + result_code + secure_token + secure_token_id + } +} +``` + +**Response:** + +```json +{ + "data": { + "createPayflowProToken": { + "response_message": "Approved", + "result": 0, + "result_code": 0, + "secure_token": "5JRGtIDsaJUuEPq0lR5m9ugqG", + "secure_token_id": "H3roFRhGjKzxCKr5TlA8mooClBpQxgBY" + } + } +} +``` + +## Input attributes + +The `createPayflowProToken` mutation requires the `cart_id` and a set of response URLs. + +### PayflowProTokenInput object + +The `PayflowProTokenInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`urls` | PayflowProUrlInput! | A set of URLs that PayPal uses for callback + +### PayflowProUrlInput object + +The `PayflowProUrlInput` object contains a set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is `https://www.example.com/path/to/page.html`, the relative URL is `path/to/page.html`. + +Use this input for Payflow Pro and Payment Pro payment methods. + +The `PayflowProUrlInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cancel_url` | String! | The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is `https://www.example.com/paypal/action/cancel.html`, the relative URL is `paypal/action/cancel.html` +`error_url` | String! | The relative URL of the transaction error page that PayPal will redirect to upon payment error. If the full URL to this page is `https://www.example.com/paypal/action/error.html`, the relative URL is `paypal/action/error.html` +`return_url` | String! | The relative URL of the final confirmation page that PayPal will redirect to upon payment success. If the full URL to this page is `https://www.example.com/paypal/action/return.html`, the relative URL is `paypal/action/return.html` + +## Output attributes + +{:.bs-callout-info} +The `createPayflowProToken` mutation previously returned a `PayflowProToken` object, which has been deprecated. The mutation now returns a `CreatePayflowProTokenOutput` object. The contents of these objects are identical. + +The `CreatePayflowProTokenOutput` object contains the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`response_message` | String! | The **RESPMSG** returned by PayPal. If the `result` is `0`, the `response_message` is `Approved` +`result` | Int! | Contains a non-zero value if any errors occurred +`result_code` | Int! | The **RESULT** returned by PayPal. A value of `0` indicates the transaction was approved +`secure_token` | String! | Secure token generated by PayPal +`secure_token_id` | String! | Secure token ID generated by PayPal + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Field PayflowProTokenInput.cart_id of required type String! was not provided.` | You must specify the `cart_id` attribute. +`Field PayflowProTokenInput.urls of required type PayflowProUrlInput! was not provided.` | You must specify the `urls` attribute. +`Field PayflowProUrlInput.return_url of required type String! was not provided.` | You must specify the `return_url` attribute. +`Field PayflowProUrlInput.error_url of required type String! was not provided.` | You must specify the `error_url` attribute. +`Field PayflowProUrlInput.cancel_url of required type String! was not provided.` | You must specify the `cancel_url` attribute. diff --git a/src/guides/v2.3/graphql/mutations/create-paypal-express-token.md b/src/guides/v2.3/graphql/mutations/create-paypal-express-token.md new file mode 100644 index 00000000000..1cb8dbdbd65 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/create-paypal-express-token.md @@ -0,0 +1,125 @@ +--- +group: graphql +title: createPaypalExpressToken mutation +--- + +The `createPaypalExpressToken` mutation begins the authorization process for the following payment methods: + +* PayPal Express Checkout +* PayPal Payflow Pro with Express Checkout +* PayPal Payflow Link with Express Checkout + +The input includes the cart ID, the payment method code, and a set of URLs that PayPal uses to respond to the token request. If the request is successful, PayPal returns a token. The [`setPaymentMethodOnCart`]({{page.baseurl}}/graphql/mutations/set-payment-method.html) mutation uses this token later in the authorization process. + +## Syntax + +```graphql +mutation { + createPaypalExpressToken( + input: PaypalExpressTokenInput! + ) { + PaypalExpressTokenOutput + } +} +``` + +## Example usage + +**Request:** + +```graphql +mutation { + createPaypalExpressToken( + input: { + cart_id: "rMQdWEecBZr4SVWZwj2AF6y0dNCKQ8uH" + code: "paypal_express" + express_button: true + urls: { + return_url: "paypal/action/return.html" + cancel_url: "paypal/action/cancel.html" + } + } + ) { + token + paypal_urls { + start + edit + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "createPaypalExpressToken": { + "token": "", + "paypal_urls": { + "start": "https://www.sandbox.paypal.com/checkoutnow?token=", + "edit": "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&useraction=continue&token=" + } + } + } +} +``` + +## Input attributes + +### PaypalExpressTokenInput {#PaypalExpressTokenInput} + +The `PaypalExpressTokenInput` object defines the attributes required to receive a payment token from PayPal. + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`code` | String! | Payment method code +`express_button` | Boolean | Indicates whether the buyer selected the PayPal Express Checkout button. The default value is `false` +`urls` | [`PaypalExpressUrlsInput!`](#PaypalExpressUrlsInput) | A set of relative URLs that PayPal will use in response to various actions during the authorization process. +`use_paypal_credit` | Boolean | Indicates whether the buyer clicked the PayPal credit button. The default value is `false` + +### PaypalExpressUrlsInput {#PaypalExpressUrlsInput} + +The `PaypalExpressUrlsInput` object contains a set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is `https://www.example.com/path/to/page.html`, the relative URL is `path/to/page.html`. Use this input for Express Checkout and Payments Standard payment methods. + +Attribute | Data Type | Description +--- | --- | --- +`cancel_url` | String! | The relative URL of the page that PayPal will redirect to when the buyer cancels the transaction in order to choose a different payment method. If the full URL to this page is `https://www.example.com/paypal/action/cancel.html`, the relative URL is `paypal/action/cancel.html`. +`pending_url` | String | The relative URL of the page that PayPal will redirect to when the payment has been put on hold for additional review. This condition mostly applies to ACH transactions, and is not applicable to most PayPal solutions. If the full URL to this page is `https://www.example.com/paypal/action/success_pending.html`, the relative URL is `paypal/action/success_pending.html`. +`return_url` | String! | The relative URL of the final confirmation page that PayPal will redirect to upon payment success. If the full URL is `https://www.example.com/paypal/action/success_review.html`, the relative URL is `paypal/action/success_review.html`. +`success_url` | String | The relative URL of the order confirmation page that PayPal will redirect to when the payment is successful and additional confirmation is not needed. Not applicable to most PayPal solutions. If the full URL to this page is `https://www.example.com/paypal/action/success.html`, the relative URL is `paypal/action/success.html`. + +## Output attributes + +{:.bs-callout-info} +The `createPaypalExpressToken` mutation previously returned a `PaypalExpressToken` object, which has been deprecated. The mutation now returns a `PaypalExpressTokenOutput` object. The contents of these objects are identical. + +### PaypalExpressTokenOutput {#PaypalExpressTokenOutput} + +The `PaypalExpressToken` object contains a token returned by PayPal and a set of URLs that allow the buyer to authorize payment and adjust checkout details. + +Attribute | Data Type | Description +--- | --- | --- +`paypal_urls` | [PaypalExpressUrlList](#PaypalExpressUrlList) | A set of URLs that allow the buyer to authorize payment and adjust checkout details +`token` | String | The token returned by PayPal + +### PaypalExpressUrlList {#PaypalExpressUrlList} + +The `PaypalExpressUrlList` object defines a set of URLs that allow the buyer to authorize payment and adjust checkout details. + +Attribute | Data Type | Description +--- | --- | --- +`edit` | String | The PayPal URL that allows the buyer to edit their checkout details +`start` | String | The URL to the PayPal login page + +## Errors + +Error | Description +--- | --- +`Required parameter "cart_id" is missing` | The mutation does not contain a `cart_id` argument. +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Field PaypalExpressTokenInput.code of required type String! was not provided.` | The required attribute `code` is missing. +`The requested Payment Method is not available.` | The payment method is not configured. +`Field PaypalExpressUrlsInput.cancel_url of required type String! was not provided.` | The required attribute `cancel_url` is missing. +`Field PaypalExpressUrlsInput.return_url of required type String! was not provided.` | The required attribute `return_url` is missing. \ No newline at end of file diff --git a/src/guides/v2.3/graphql/mutations/delete-customer-address.md b/src/guides/v2.3/graphql/mutations/delete-customer-address.md new file mode 100644 index 00000000000..32950ac1efd --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/delete-customer-address.md @@ -0,0 +1,74 @@ +--- +group: graphql +title: deleteCustomerAddress mutation +--- + +Use the `deleteCustomerAddress` mutation to delete the specified customer address. + +To return or modify information about a customer, we recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +```graphql +mutation { + deleteCustomerAddress( + id: Int! + ) { + Boolean + } +} +``` + +## Example usage + +The following call deletes a customer's address. + +**Request:** + +```graphql +mutation { + deleteCustomerAddress(id: 4) +} +``` + +**Response:** + +```json +{ + "data": { + "deleteCustomerAddress": true + } +} +``` + +## Input attributes + +The `deleteCustomerAddress` mutation requires the following input: + +Attribute | Data Type | Description +--- | --- | --- +`id` | Int! | The ID assigned to the address object + +## Output attributes + +The `deleteCustomerAddress` mutation returns a Boolean value that indicates whether the operation was successful. + +## Errors + +Error | Description +--- | --- +`Address "id" value should be specified` | The `id` argument is zero. +`Could not find a address with ID "XXX"` | The customer address specified in the `id` argument does not exist. +`Customer Address XXX is set as default billing address and cannot be deleted` | You cannot delete a default billing address. +`Customer Address XXX is set as default shipping address and cannot be deleted` | You cannot delete a default shipping address. +`Field "deleteCustomerAddress" argument "id" requires type Int!, found "XXX".` | The specified `id` argument value has the wrong type. +`Syntax Error: Expected Name, found )` | The `id` argument was omitted or does not have a value. +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. + +## Related topics + +* [customer query]({{page.baseurl}}/graphql/queries/customer.html) +* [createCustomer mutation]({{page.baseurl}}/graphql/mutations/create-customer.html) +* [updateCustomer mutation]({{page.baseurl}}/graphql/mutations/update-customer.html) +* [createCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/create-customer-address.html) +* [updateCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/update-customer-address.html) diff --git a/src/guides/v2.3/graphql/mutations/delete-payment-token.md b/src/guides/v2.3/graphql/mutations/delete-payment-token.md new file mode 100644 index 00000000000..912c46a2dba --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/delete-payment-token.md @@ -0,0 +1,100 @@ +--- +group: graphql +title: deletePaymentToken mutation +--- + +The `deletePaymentToken` mutation deletes a payment token from the system. Use the [customerPaymentTokens query]({{page.baseurl}}/graphql/queries/customer-payment-tokens.html) to retrieve all stored payment methods associated with a particular customer. + +{:.bs-callout-info} +You must specify the customer's authorization token in the header of the call. + +## Syntax + +```graphql +mutation { + deletePaymentToken( + public_hash: String! + ) { + DeletePaymentTokenOutput + } +} +``` + +## Example usage + +The following example deletes the Discover Card listed in the results of the `customerPaymentTokens` query. The `public_hash` you specify will be unique to your application. + +**Request:** + +```graphql +mutation { + deletePaymentToken( + public_hash: "377c1514e0..." + ) { + result + customerPaymentTokens { + items { + details + public_hash + payment_method_code + type + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "deletePaymentToken": { + "result": true, + "customerPaymentTokens": { + "items": [ + { + "details": "{\"type\":\"VI\",\"maskedCC\":\"1111\",\"expirationDate\":\"09\\/2022\"}", + "public_hash": "f5816fe2ab...", + "payment_method_code": "braintree", + "type": "card" + } + ] + } + } + } +} +``` +## Input attributes + +The `deletePaymentToken` object must contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`public_hash` | `String!` | The public hash of the token + +## Output attributes + +The top-level `DeletePaymentTokenOutput` object is listed first. All child objects are listed in alphabetical order. + +### DeletePaymentTokenOutput attributes + +The `DeletePaymentTokenOutput` object returns the result of the operation and details about the remaining customer payment tokens. + +Attribute | Data Type | Description +--- | --- | --- +`customerPaymentTokens` | `CustomerPaymentTokens` | Contains an array of customer payment tokens +`result` | Boolean! | A value of `true` indicates the request was successful + +{% include graphql/customer-payment-tokens.md %} + +## Errors + +Error | Description +--- | --- +`Could not find a token using public hash: xxxxxxxx` | The customer token specified in the `public_hash` argument does not exist in the `vault_payment_token` table. +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. + +## Related topics + +[customerPaymentTokens query]({{page.baseurl}}/graphql/queries/customer-payment-tokens.html) diff --git a/src/guides/v2.3/graphql/mutations/generate-customer-token.md b/src/guides/v2.3/graphql/mutations/generate-customer-token.md new file mode 100644 index 00000000000..8226ab485f0 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/generate-customer-token.md @@ -0,0 +1,80 @@ +--- +group: graphql +title: generateCustomerToken mutation +--- + +Use the `generateCustomerToken` mutation to create a new customer token. + +To return or modify information about a customer, we recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +```graphql +mutation { + generateCustomerToken( + email: String! + password: String! + ) { + CustomerToken + } +} +``` + +## Example usage + +The following call creates a new customer token. + +**Request:** + +```graphql +mutation { + generateCustomerToken( + email: "bobloblaw@example.com" + password: "b0bl0bl@w" + ) { + token + } +} +``` + +**Response:** + +```json +{ + "data": { + "generateCustomerToken": { + "token": "ar4116zozoagxty1xjn4lj13kim36r6x" + } + } +} +``` + +## Input attributes + +The `generateCustomerToken` mutation requires the following inputs: + +Attribute | Data Type | Description +--- | --- | --- +`email` | String | The customer's email address +`password` | String | The customer's password + +## Output attributes + +The `generateCustomerToken` mutation returns a valid token for the customer. + +Attribute | Data Type | Description +--- | --- | --- +`token` | String | The customer token + +## Errors + +Error | Description +--- | --- +`Specify the "email" value.` | The value specified in the `email` argument is empty. +`Specify the "password" value.` | The value specified value in the `password` argument is empty. +`The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later.` | Authentication error. The email or password is incorrect, or the customer account is locked. + +## Related topics + +* [customer query]({{page.baseurl}}/graphql/queries/customer.html) +* [revokeCustomerToken mutation]({{page.baseurl}}/graphql/mutations/revoke-customer-token.html) diff --git a/src/guides/v2.3/graphql/mutations/handle-payflow-pro-response.md b/src/guides/v2.3/graphql/mutations/handle-payflow-pro-response.md new file mode 100644 index 00000000000..ca3540b8159 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/handle-payflow-pro-response.md @@ -0,0 +1,91 @@ +--- +group: graphql +title: handlePayflowProResponse mutation +--- + +The `handlePayflowProResponse` mutation sends the silent post data that the client received from the Payflow Pro gateway to the Magento server. The content of this payload varies based on factors such as the merchant's location, the items purchased, and the billing/shipping addresses. The following is an example payload: + +```text +'BILLTOCITY=CityM&AMT=0.00&BILLTOSTREET=Green+str,+67&VISACARDLEVEL=12&SHIPTOCITY=CityM' +'&NAMETOSHIP=John+Smith&ZIP=75477&BILLTOLASTNAME=Smith&BILLTOFIRSTNAME=John' +'&RESPMSG=Verified&PROCCVV2=M&STATETOSHIP=AL&NAME=John+Smith&BILLTOZIP=75477&CVV2MATCH=Y' +'&PNREF=B70CCC236815&ZIPTOSHIP=75477&SHIPTOCOUNTRY=US&SHIPTOSTREET=Green+str,+67&CITY=CityM' +'&HOSTCODE=A&LASTNAME=Smith&STATE=AL&SECURETOKEN=MYSECURETOKEN&CITYTOSHIP=CityM&COUNTRYTOSHIP=US' +'&AVSDATA=YNY&ACCT=1111&AUTHCODE=111PNI&FIRSTNAME=John&RESULT=0&IAVS=N&POSTFPSMSG=No+Rules+Triggered&' +'BILLTOSTATE=AL&BILLTOCOUNTRY=US&EXPDATE=0222&CARDTYPE=0&PREFPSMSG=No+Rules+Triggered&SHIPTOZIP=75477&' +'PROCAVS=A&COUNTRY=US&AVSZIP=N&ADDRESS=Green+str,+67&BILLTONAME=John+Smith&' +'ADDRESSTOSHIP=Green+str,+67&' +'AVSADDR=Y&SECURETOKENID=MYSECURETOKENID&SHIPTOSTATE=AL&TRANSTIME=2019-06-24+07%3A53%3A10' +``` + +See [Paypal Payflow Pro payment method]({{page.baseurl}}/graphql/payment-methods/payflow-pro.html) for detailed information about the workflow of PayPal Payflow Pro transactions. + +## Syntax + +```graphql +mutation { + handlePayflowProResponse( + input: PayflowProResponseInput! + ) { + PayflowProResponseOutput + } +} +``` + +## Example usage + +The following example sends the Payflow Pro payload to Magento: + +**Request:** + +```graphql +mutation { + handlePayflowProResponse( + input: { + cart_id: "Po1WkfK7d3vZE0qga610NwJIbxgqllpt" + paypal_payload: "$payload" + } + ) { + cart { + selected_payment_method { + code + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "handlePayflowProResponse": { + "cart": { + "selected_payment_method": { + "code": "payflowpro", + } + } + } + } +} +``` + +## Input attributes + +The `PayflowProResponseInput` object must contain the `cart_id` and `paypal_payload` attributes. + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`paypal_payload` | String! | The payload returned from PayPal + +## Output attributes + +The PayflowProResponseOutput contains a `Cart` object. + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. diff --git a/src/guides/v2.3/graphql/mutations/index.md b/src/guides/v2.3/graphql/mutations/index.md new file mode 100644 index 00000000000..33c99e47ef7 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/index.md @@ -0,0 +1,141 @@ +--- +group: graphql +title: Using mutations +redirect_from: + - /guides/v2.3/graphql/mutations.html +--- + +While GraphQL queries perform read operations, mutations change the data. A mutation can create, update, or delete objects and fields. In REST terminology, queries operate like `GET` requests, while mutations are similar to `POST`, `PUT`, and `DELETE`. + +## Structure of a mutation + +A mutation contains the following elements: + +* The keyword `mutation` +* An operation name for your local implementation. This name is required if you include variables. Otherwise, it is optional. +* The mutation name +* The input object or attributes. Most mutations require an input object that contains data or individual attributes for the Magento server to process. However, some mutations, such as `createEmptyCart`, do not require an input object. In this particular case, the authorization token passed with the request provides the needed context. +* The output object, which specifies which data the mutation returns. + +The following example shows the structure of the `createCustomer` mutation: + +```graphql +mutation myCreateCustomer { + createCustomer( + input: CustomerInput! + ) { + CustomerOutput + } +} +``` + +In this example, `myCreateCustomer` identifies your implementation. `CustomerInput` is a non-nullable object that defines a customer. (The exclamation point indicates the value is non-nullable.) The `CustomerOutput` object defines which fields to return. + +Now let's take a look at a fully-defined mutation. This time, we'll specify the minimum fields needed as input to create a customer (`firstname`, `lastname`, `email`, and `password`). We could include the same fields in the output, but GraphQL allows you to return only the data you need, which is the customer `email`. + +```graphql +mutation myCreateCustomerNoVariables { + createCustomer( + input: { + firstname: "Melanie" + lastname: "Shaw" + email: "mshaw@example.com" + password: "Password1" + } + ) { + customer { + email + } + } +} +``` + +The mutation returns the customer email: + +```json +{ + "data": { + "createCustomer": { + "customer": { + "email" : "mshaw@example.com" + } + } + } +} +``` + +## Mutation input + +A mutation can require either an object as input (as shown above) or one or more scalar values. When specifying an object, you must include the `input: {}` keyword. When the mutation requires scalar values, specify the field name and value, as shown below: + +```graphql +mutation myGenerateCustomerToken { + generateCustomerToken( + email: "mshaw@example.com" + password: "Password1" + ) { + token + } +} +``` + +## Mutation variables + +Specifying variables in a mutation can help increase code re-use. Consider the following requirements when generating a mutation that contains one or more variables: + +* All variables must be declared up-front, immediately after the operation name. +* Variables are typed: they can be scalar or an object. +* You must use all declared variables. Object variables are defined in JSON. + +The following example declares the `$CustomerInput` variable. It is referenced in the `input` statement. + +```graphql +mutation myCreateCustomerWithVariables($CustomerInput: CustomerInput!) { + createCustomer( + input: $CustomerInput + ) { + customer { + email + } + } +} +``` + +The `$CustomerInput` variable is defined as a JSON object: + +```json +{ + "CustomerInput": { + "firstname": "Melanie", + "lastname": "Shaw", + "email": "mshaw@example.com", + "password": "Password1" + } +} +``` + +This example updates the customer's email using two scalar variables (`$email`, `$password`). + +```graphql +mutation myUpdateCustomer($email: String!, $password: String!) { + updateCustomer( + input: { + email: $email + password: $password + } + ) { + customer { + email + } + } +} +``` + +The variables are defined separately. + +```json +{ + "email": "melanie.shaw@example.com", + "password": "Password1" +} +``` diff --git a/src/guides/v2.3/graphql/mutations/merge-carts.md b/src/guides/v2.3/graphql/mutations/merge-carts.md new file mode 100644 index 00000000000..99a4c9b9ce6 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/merge-carts.md @@ -0,0 +1,104 @@ +--- +group: graphql +title: mergeCarts mutation +--- + +The `mergeCarts` mutation transfers the contents of a guest cart into the cart of a logged-in customer. This mutation must be run on behalf of a logged-in customer. + +The mutation retains any items that were already in the logged-in customer's cart. If both the guest and customer carts contain the same item, `mergeCarts` adds the quantities. Upon success, the mutation deletes the original guest cart. + +Use the [`customerCart` query]({{page.baseurl}}/graphql/queries/customer-cart.html) to determine the value of the `destination_cart_id` attribute. + +## Syntax + +```graphql +mutation { + mergeCarts( + source_cart_id: String! + destination_cart_id: String! + ) { + Cart! + } +} +``` + +## Example usage + +In the following example, the customer had one Overnight Duffle in the cart (`CYmiiQRjPVc2gJUc5r7IsBmwegVIFO43`) before a guest cart (`mPKE05OOtcxErbk1Toej6gw6tcuxvT9O`) containing a Radiant Tee and another Overnight Duffle was merged. The cart now includes three items, including two Overnight Duffles. + +**Request:** + +```graphql +mutation { + mergeCarts(source_cart_id: "mPKE05OOtcxErbk1Toej6gw6tcuxvT9O", destination_cart_id: "CYmiiQRjPVc2gJUc5r7IsBmwegVIFO43") { + items { + id + product { + name + sku + } + quantity + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "mergeCarts": { + "items": [ + { + "id": "14", + "product": { + "name": "Overnight Duffle", + "sku": "24-WB07" + }, + "quantity": 2 + }, + { + "id": "17", + "product": { + "name": "Radiant Tee", + "sku": "WS12" + }, + "quantity": 1 + } + ] + } + } +} +``` + +## Input attributes + +Attribute | Data Type | Description +--- | --- | --- +`destination_cart_id` | String! | The ID of the logged-in customer's cart +`source_cart_id` | String! | The ID of the guest cart + +## Output attributes + +The `mergeCarts` mutation returns a `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Current user does not have an active cart.` | The `mergeCarts` mutation deactivates the guest cart specified in the `source_cart_id` after merging. The guest cannot make any further operations with it. +`Required parameter "destination_cart_id" is missing` | The `destination_cart_id` attribute contains an empty value. +`Required parameter "source_cart_id" is missing` | The `source_cart_id` attribute contains an empty value. +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table, or you tried to merge two guest carts. +`The current user cannot perform operations on cart` | The authorized customer tried to merge a guest cart into the cart of another customer. diff --git a/src/guides/v2.3/graphql/mutations/place-order.md b/src/guides/v2.3/graphql/mutations/place-order.md new file mode 100644 index 00000000000..391f33fff60 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/place-order.md @@ -0,0 +1,99 @@ +--- +group: graphql +title: placeOrder mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-place-order.html +--- + +The `placeOrder` mutation converts the cart into an order and returns an order ID. You cannot manage orders with GraphQL, because orders are part of the backend. You can use REST or SOAP calls to manage orders to their completion. + +Perform the following actions before using the `placeOrder` mutation: + +- Create an empty cart +- Add one or more products to the cart +- Set the billing address +- Set the shipping address +- Set the shipping method +- Set the payment method +- For guest customers, assign an email to the cart + +## Syntax + +```graphql +mutation { + placeOrder( + input: PlaceOrderInput + ) { + PlaceOrderOutput + } +} +``` + +## Example usage + +**Request:** + +```graphql +mutation { + placeOrder( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + } + ) { + order { + order_number + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "placeOrder": { + "order": { + "order_number": "000000006" + } + } + } +} +``` + +## Input attributes + +The `placeOrderInput` object must contain the following attribute: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer’s cart + +## Output attributes + +The `placeOrderOutput` object returns an `order` object. + +Attribute | Data Type | Description +--- | --- | --- +`order` | Order! | The unique ID that identifies the order + +### Order object + +Attribute | Data Type | Description +--- | --- | --- +`order_id` | String! | Deprecated. Use `order_number` instead. The unique ID that identifies the order +`order_number` | String | The unique ID that identifies the order + +## Errors + +Error | Description +--- | --- +`Enter a valid payment method and try again` | The payment method was not set. See [setPaymentMethodOnCart]({{ page.baseurl }}/graphql/mutations/set-payment-method.html) mutation. +`Guest email for cart is missing.` | The guest attempted to place an order but did not provide an email address. See [setGuestEmailOnCart]({{ page.baseurl }}/graphql/mutations/set-guest-email.html) mutation. +`Please check the billing address information` | The billing address was not set. See [setBillingAddressOnCart]({{ page.baseurl }}/graphql/mutations/set-billing-address.html) mutation. +`Required parameter "cart_id" is missing` | The mutation does not contain a `cart_id` parameter. +`Some addresses can't be used due to the configurations for specific countries` | The shipping method was not set. See [setShippingMethodsOnCart]({{ page.baseurl }}/graphql/mutations/set-shipping-method.html) mutation. +`Some of the products are out of stock` | One of the products in the shopping cart are currently out of stock. +`The current user cannot perform operations on cart` | An unauthorized user (guest) tried to place an order on behalf of an authorized user (customer), or a customer tried to place an order on behalf of another customer. +`The shipping method is missing. Select the shipping method and try again` | The shipping method was not set. See [setShippingMethodsOnCart]({{ page.baseurl }}/graphql/mutations/set-shipping-method.html) mutation. +`Unable to place order: A server error stopped your order from being placed. Please try to place your order again` | The shopper tried to place an order when no products are in the shopping cart. diff --git a/src/guides/v2.3/graphql/mutations/redeem-giftcard-balance.md b/src/guides/v2.3/graphql/mutations/redeem-giftcard-balance.md new file mode 100644 index 00000000000..80d1d1bb1fd --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/redeem-giftcard-balance.md @@ -0,0 +1,89 @@ +--- +group: graphql +title: redeemGiftCardBalanceAsStoreCredit mutation +ee_only: True +--- + +The `redeemGiftCardBalanceAsStoreCredit` mutation converts the entire balance of a gift card to store credit. The gift card must be redeemable and cannot have a balance of 0 at the time you run the mutation. After successfully running the mutation, the value of the gift card changes to 0. + +{:.bs-callout-info} +Run this mutation on behalf of logged-in customers only. [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) describes how to send a request as a customer. + +## Syntax + +```graphql +mutation { + redeemGiftCardBalanceAsStoreCredit( + input: GiftCardAccountInput + ) { + GiftCardAccount + } +} +``` + +## Example usage + +The following example redeems the gift card with code `“056MHP57TJ5C”`. + +**Request:** + +```graphql +mutation { + redeemGiftCardBalanceAsStoreCredit( + input: { + gift_card_code: "056MHP57TJ5C" + } + ) { + balance { + currency + value + } + code + expiration_date + } +} +``` + +**Response:** + +```json +{ + "data": { + "redeemGiftCardBalanceAsStoreCredit": { + "balance": { + "currency": "USD", + "value": 0 + }, + "code": "056MHP57TJ5C", + "expiration_date": null + } + } +} +``` + +## Input attributes + +### GiftCardAccountInput object {#GiftCardAccountInput} + +The `GiftCardAccountInput` object must contain the following attribute: + +Attribute | Data Type | Description +--- | --- | --- +`gift_card_code` | String! | The gift card code + +## Output attributes + +The `GiftCardAccount` object contains the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`balance` | Money | The remaining balance of the gift card, including the currency +`code` | String | The gift card code +`expiration_date` | String | The date when the gift card expires, if any + +## Errors + +Error | Description +--- | --- +`Gift card not found` | The specified `gift_card_code` value does not exist in the `giftcardaccount` table or the amount has been already redeemed. +`Field GiftCardAccountInput.gift_card_code of required type String! was not provided` | The value specified in the `GiftCardAccountInput.gift_card_code` argument is empty. diff --git a/src/guides/v2.3/graphql/mutations/remove-coupon.md b/src/guides/v2.3/graphql/mutations/remove-coupon.md new file mode 100644 index 00000000000..1ac36411dfa --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/remove-coupon.md @@ -0,0 +1,122 @@ +--- +group: graphql +title: removeCouponFromCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-remove-coupon.html +--- + +The `removeCouponFromCart` mutation removes a previously-applied coupon from the cart. The cart must contain at least one item in order to remove the coupon. + +## Syntax + +```graphql +mutation { + removeCouponFromCart( + input: RemoveCouponFromCartInput + ) { + RemoveCouponFromCartOutput + } +} +``` + +## Example usage + +The following example removes a coupon from the cart. + +**Request:** + +```graphql +mutation { + removeCouponFromCart( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + } + ) { + cart { + items { + product { + name + } + quantity + } + applied_coupons { + code + } + prices { + grand_total { + value + currency + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "removeCouponFromCart": { + "cart": { + "items": [ + { + "product": { + "name": "Strive Shoulder Pack" + }, + "quantity": 1 + }, + { + "product": { + "name": "Affirm Water Bottle " + }, + "quantity": 1 + } + ], + "applied_coupons": null, + "prices": { + "grand_total": { + "value": 39, + "currency": "USD" + } + } + } + } + } +} +``` + +## Input attributes + +The `removeCouponFromCart` mutation must contain the following attribute: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart + +## Output attributes + +The `removeCouponFromCart` mutation returns the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Cart does not contain products.` | The coupon cannot be removed from the empty cart. +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Current user does not have an active cart.` | The user cannot perform this mutation on the inactive cart. +`Required parameter "cart_id" is missing` | The required `cart_id` argument contains an empty value. +`The coupon code couldn't be deleted. Verify the coupon code and try again.` | The coupon was not removed from the cart. Check the existing shopping cart price rules for details. +`The current user cannot perform operations on cart XXX` | An unauthorized user (guest) tried to add the product into a customer's cart, or an authorized user (customer) tried to add the product into the cart of another customer. +`Wrong store code specified for cart` | The specified `cart_id` does not exist in specified store. diff --git a/src/guides/v2.3/graphql/mutations/remove-giftcard.md b/src/guides/v2.3/graphql/mutations/remove-giftcard.md new file mode 100644 index 00000000000..8f3951fb9ae --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/remove-giftcard.md @@ -0,0 +1,86 @@ +--- +group: graphql +title: removeGiftCardFromCart mutation +ee_only: True +--- + +The `removeGiftCardFromCart` mutation removes a previously-applied gift card from the cart. + +## Syntax + +```graphql +mutation { + removeGiftCardFromCart( + input: RemoveGiftCardFromCartInput + ) { + RemoveGiftCardFromCartOutput + } +} +``` + +## Example usage + + The following example removes a gift card from the cart. + +**Request:** + +```graphql +mutation { + removeGiftCardFromCart( + input: { + cart_id: "lOeLKsVkZ1PEvA8A7EaCvmEAk4JRBR7A" + gift_card_code: "049XDMZ6L81X" + } + ) { + cart { + applied_gift_cards { + code + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "removeGiftCardFromCart": { + "cart": { + "applied_gift_cards": [] + } + } + } +} +``` + +## Input attributes + +The `removeGiftCardFromCartInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`gift_card_code` | String! | The gift card code + +## Output attributes + +The `removeGiftCardFromCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + + {% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID \"xxxxx\"` | The ID provided in the `cart_id` field is invalid or the cart does not exist for the customer. +`The gift card couldn't be deleted from the quote.` | The value provided in the `gift_card_code` field is invalid or the gift card with that specific ID does not exist in the cart. diff --git a/src/guides/v2.3/graphql/mutations/remove-item.md b/src/guides/v2.3/graphql/mutations/remove-item.md new file mode 100644 index 00000000000..fa93ec6bb2d --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/remove-item.md @@ -0,0 +1,116 @@ +--- +group: graphql +title: removeItemFromCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-remove-item.html +--- + +The `removeItemFromCart` mutation deletes the entire quantity of a specified item from the cart. If you remove all items from the cart, the cart continues to exist. + +## Syntax + +```graphql +mutation { + removeItemFromCart( + input: RemoveItemFromCartInput + ) { + RemoveItemFromCartOutput + } +} +``` + +## Example usage + +The following example removes cart item 14 from the cart. + +**Request:** + +```graphql +mutation { + removeItemFromCart( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG", + cart_item_id: 14 + } + ) { + cart { + items { + id + product { + name + } + quantity + } + prices { + grand_total { + value + currency + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "removeItemFromCart": { + "cart": { + "items": [ + { + "id": "13", + "product": { + "name": "Strive Shoulder Pack" + }, + "quantity": 3 + } + ], + "prices": { + "grand_total": { + "value": 96, + "currency": "USD" + } + } + } + } + } +} +``` + +## Input attributes + +### RemoveItemFromCartInput object {#RemoveItemFromCartInput} + +The `RemoveItemFromCartInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`cart_item_id` | Int! | The unique ID assigned when a customer places an item in the cart + +## Output attributes + +The `RemoveItemFromCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +## Errors + +Error | Description +--- | --- +`Cart doesn't contain the ZZZ item.` | The item ID specified in the `cart_item_id` argument does not exist in the requested shopping cart. +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Required parameter "cart_id" is missing.` | The value specified in the `cart_id` argument is empty. +`Required parameter "cart_item_id" is missing.` | The value specified in the `cart_item_id` argument is equal to zero. +`The current user cannot perform operations on cart "XXX"` | An unauthorized user (guest) tried to remove a product from the shopping cart of authorized user (customer), or a customer tried to remove a product from the shopping cart of another customer. + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. diff --git a/src/guides/v2.3/graphql/mutations/remove-store-credit.md b/src/guides/v2.3/graphql/mutations/remove-store-credit.md new file mode 100644 index 00000000000..6f59bfa570d --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/remove-store-credit.md @@ -0,0 +1,105 @@ +--- +group: graphql +title: removeStoreCreditFromCart mutation +ee_only: true +--- + +The `removeStoreCreditFromCart` mutation removes store credit previously applied to the specified cart with the [`applyStoreCreditToCart`]({{page.baseurl}}/graphql/mutations/apply-store-credit.html) mutation. Magento restores the customer's available store credit to its original amount and recalculates all cart totals. + +Store credit must be enabled on the store to run this mutation. + +## Syntax + +```graphql +mutation { + removeStoreCreditFromCart( + input: RemoveStoreCreditFromCartInput + ) { + RemoveStoreCreditFromCartOutput + } +} +``` + +## Example usage + +**Request:** + +```graphql +mutation { + removeStoreCreditFromCart( + input: { + cart_id: "4HHaKzxpKM2ZwD0IcheRfcPNBWS3OvRM" + } + ) { + cart { + applied_store_credit { + applied_balance { + currency + value + } + current_balance { + currency + value + } + } + prices { + grand_total { + currency + value + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "removeStoreCreditFromCart": { + "cart": { + "applied_store_credit": { + "applied_balance": { + "currency": "USD", + "value": 0 + }, + "current_balance": { + "currency": "USD", + "value": 10 + } + }, + "prices": { + "grand_total": { + "currency": "USD", + "value": 34.64 + } + } + } + } + } +} +``` + +## Input attributes + +The `RemoveStoreCreditFromCartInput` object must contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer’s cart + +## Output attributes + +The `RemoveStoreCreditFromCartOutput` object returns the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. diff --git a/src/guides/v2.3/graphql/mutations/revoke-customer-token.md b/src/guides/v2.3/graphql/mutations/revoke-customer-token.md new file mode 100644 index 00000000000..07360fc119e --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/revoke-customer-token.md @@ -0,0 +1,61 @@ +--- +group: graphql +title: revokeCustomerToken mutation +--- + +Use the `revokeCustomerToken` mutation to revokes the customer's token. + +To return or modify information about a customer, we recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +```graphql +mutation { + revokeCustomerToken { + RevokeCustomerTokenOutput + } +} +``` + +## Example usage + +The following call revokes the customer's token. + +**Request:** + +```graphql +mutation { + revokeCustomerToken { + result + } +} +``` + +**Response:** + +```json +{ + "data": { + "revokeCustomerToken": { + "result": true + } + } +} +``` + +## Output attributes + +Attribute | Data Type | Description +--- | --- | --- +`result` | Boolean! | Returns `true` if the token was successfully revoked + +## Errors + +Error | Description +--- | --- +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. + +## Related topics + +* [customer query]({{page.baseurl}}/graphql/queries/customer.html) +* [generateCustomerToken mutation]({{page.baseurl}}/graphql/mutations/generate-customer-token.html) diff --git a/src/guides/v2.3/graphql/mutations/send-email-to-friend.md b/src/guides/v2.3/graphql/mutations/send-email-to-friend.md new file mode 100644 index 00000000000..665775fe67f --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/send-email-to-friend.md @@ -0,0 +1,153 @@ +--- +group: graphql +title: sendEmailToFriend mutation +--- + +Use the `sendEmailToFriend` mutation to allow Magento to send a message on behalf of a customer to the specified email addresses. + +{:.bs-callout-info} +The **Stores** > **Configuration** > **Catalog** > **Email to a friend** > **Enabled** field must be set to **Yes** to implement this mutation. + +## Syntax + +```graphql +mutation { + sendEmailToFriend( + input: SendEmailToFriendInput + ) { + SendEmailToFriendOutput + } +} +``` + +## Example usage + +The following example sends a message to two friends. + +**Request:** + +```graphql +mutation { + sendEmailToFriend( + input: { + product_id: 10 + sender: { + name: "Veronica Cost" + email: "roni_cost@example.com" + message: "Sarah needs this! http://luma.example.com/savvy-shoulder-tote.html" + } + recipients: [ + { name: "Amie Franklin", email: "afranklin@example.com" } + { name: "Tomoko", email: "tomoko@example.com" } + ] + } + ) { + sender { + name + email + } + recipients { + name + email + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "sendEmailToFriend": { + "sender": { + "name": "Veronica Cost", + "email": "roni_cost@example.com", + }, + "recipients": [ + { + "name": "Amie Franklin", + "email": "afranklin@example.com" + }, + { + "name": "Tomoko", + "email": "tomoko@example.com" + } + ] + } + } +} +``` + +## Input attributes + +The `SendEmailToFriendInput` object contains the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`product_id` | Int! | The ID of the product that the customer is referencing +`recipients` | [SendEmailToFriendRecipientInput]! | An array containing information about each recipient +`sender` | SendEmailToFriendSenderInput! | Information about the customer and the content of the message + +### SendEmailToFriendRecipientInput object {#SendEmailToFriendRecipientInput} + +The `SendEmailToFriendRecipientInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`email` | String! | The email address of the recipient +`name` | String! | The name of the recipient + +### SendEmailToFriendSenderInput object {#SendEmailToFriendSenderInput} + +The `SendEmailToFriendSenderInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`email` | String! | The email address of the sender +`message` | String! | The text of the message to be sent +`name` | String! | The name of the sender + +## Output attributes + +The `SendEmailToFriendOutput` object contains the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`recipients` | [SendEmailToFriendRecipient] | An array containing information about each recipient +`sender` | SendEmailToFriendSender | Information about the customer and the content of the message + +### SendEmailToFriendRecipient object + +The `SendEmailToFriendRecipientInput` object can contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`email` | String | The email address of the recipient +`name` | String | The name of the recipient + +### SendEmailToFriendSender object + +The `SendEmailToFriendSender` object can contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`email` | String | The email address of the sender +`message` | String | The text of the message +`name` | String | The name of the sender + +## Errors + +Some errors occur because the **Email a friend** feature is not properly configured. Go to **Stores** > **Configuration** > **Catalog** > **Email to a friend** in the Admin to adjust the settings. + +Error | Description +--- | --- +`"Email to a Friend" is not enabled.` | "Email to a Friend" is disabled. To activate it, use the Admin to set the **Enabled** field to **Yes**. +`Please provide Name of sender.` | The value specified in the `input`.`sender`.`name` argument is empty. +`Please provide Email of sender.` | The value specified in the `input`.`sender`.`email` argument is empty. +`Please provide Message.` | The value specified in the `input`.`sender`.`message` argument is empty. +`Please provide Name for all of recipients.` | The value specified in the `input`.`recipients`[].`name` argument is empty. +`Please provide Email for all of recipients.` | The value specified in the `input`.`recipients`[].`email` argument is empty. +`The current customer isn't authorized.` | "Email to a Friend" is available for registered users only. To make it available for guests, use the Admin to set the **Allow for Guests** option to **Yes**. +`The product that was requested doesn't exist. Verify the product and try again.` | The product specified in the `product_id` argument is not visible in the current website. +`You can't send messages more than XXX times an hour.` | The user cannot send more messages in an hour than specified in the **Max Products Sent in 1 Hour** option in the Admin. diff --git a/src/guides/v2.3/graphql/mutations/set-billing-address.md b/src/guides/v2.3/graphql/mutations/set-billing-address.md new file mode 100644 index 00000000000..e2d49d8e9d0 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/set-billing-address.md @@ -0,0 +1,142 @@ +--- +group: graphql +title: setBillingAddressOnCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-set-billing-address.html +--- + +The `setBillingAddressOnCart` mutation sets the billing address for a specific cart. If you set the `same_as_shipping` attribute to `true`, Magento assigns the same address as the shipping address. + +## Syntax + +```graphql +mutation { + setBillingAddressOnCart( + input: SetBillingAddressOnCartInput + ) { + SetBillingAddressOnCartOutput + } +} +``` + +## Example usage + +The following example creates a new billing address for a specific cart. + +**Request:** + +```graphql +mutation { + setBillingAddressOnCart( + input: { + cart_id: "4JQaNVJokOpFxrykGVvYrjhiNv9qt31C" + billing_address: { + address: { + firstname: "Bob" + lastname: "Roll" + company: "Magento" + street: ["Magento Pkwy", "Main Street"] + city: "Austin" + region: "TX" + postcode: "78758" + country_code: "US" + telephone: "8675309" + save_in_address_book: true + } + same_as_shipping: false + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + region{ + code + label + } + postcode + telephone + country{ + code + label + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setBillingAddressOnCart": { + "cart": { + "billing_address": { + "firstname": "Bob", + "lastname": "Roll", + "company": "Magento", + "street": [ + "Magento Pkwy", + "Main Street" + ], + "city": "Austin", + "region": { + "code": "TX", + "label": "Texas" + }, + "postcode": "78758", + "telephone": "8675309", + "country": { + "code": "US", + "label": "US" + } + } + } + } + } +} +``` + +## Input attributes + +The top-level `SetBillingAddressOnCartInput` object is listed first. All child objects are listed in alphabetical order. + +### SetBillingAddressOnCartInput object {#SetBillingAddressOnCartInput} + +Attribute | Data Type | Description +--- | --- | --- +`billing_address` | [BillingAddressInput!](#BillingAddressInput) | The billing address for a specific cart +`cart_id` | String! | The unique ID that identifies the customer's cart + +### BillingAddressInput object {#BillingAddressInput} + +Attribute | Data Type | Description +--- | --- | --- +`address` | [CartAddressInput](#CartAddressInput) | The billing address for the cart +`customer_address_id` | Int | The unique ID that identifies the customer's address +`same_as_shipping` | Boolean | Specifies whether to use the shipping address for the billing address +`use_for_shipping` | Boolean | Deprecated. Use `same_as_shipping` instead + +### CartAddressInput object {#CartAddressInput} + +{% include graphql/cart-address-input.md %} + +## Output attributes + +The `SetBillingAddressOnCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. diff --git a/src/guides/v2.3/graphql/mutations/set-guest-email.md b/src/guides/v2.3/graphql/mutations/set-guest-email.md new file mode 100644 index 00000000000..5408d9ba3bd --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/set-guest-email.md @@ -0,0 +1,89 @@ +--- +group: graphql +title: setGuestEmailOnCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-set-guest-email.html +--- + +For guest customers, you must assign an email to the cart before you place the order. + +A logged-in customer specifies an email address when they create an account. Therefore, you can place the order without explicitly setting the email. + +## Syntax + +```graphql +mutation { + setGuestEmailOnCart( + input: SetGuestEmailOnCartInput + ) { + SetGuestEmailOnCartOutput + } +} +``` + +## Example usage + +**Request:** + +```graphql +mutation { + setGuestEmailOnCart( + input: { + cart_id: "4JQaNVJokOpFxrykGVvYrjhiNv9qt31C" + email: "jdoe@example.com" + } + ) { + cart { + email + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setGuestEmailOnCart": { + "cart": { + "email": "jdoe@example.com" + } + } + } +} +``` + +## Input attributes + +The `SetGuestEmailOnCartInput` object must contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer’s cart +`email` | String! | The guest user's email + +## Output attributes + +The `SetGuestEmailOnCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The ID specified in the `cart` argument does not exist. +`Invalid email format` | The value specified in the `email` argument has an incorrect format. +`Required parameter "cart_id" is missing` | The `cart_id` argument was omitted or contains an empty value. +`Required parameter "email" is missing` | The `email` argument was omitted or contains an empty value. +`The current user cannot perform operations on cart "XXX"` | An unauthorized user (guest) tried to set the email address on the customer's cart. +`The request is not allowed for logged in customers` | An authorized user (customer) is not allowed to use the `setGuestEmailOnCart` mutation. diff --git a/src/guides/v2.3/graphql/mutations/set-payment-method.md b/src/guides/v2.3/graphql/mutations/set-payment-method.md new file mode 100644 index 00000000000..24664126095 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/set-payment-method.md @@ -0,0 +1,135 @@ +--- +group: graphql +title: setPaymentMethodOnCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-payment-method.html +--- + +The `setPaymentMethodOnCart` mutation defines which payment method to apply to the cart. Magento GraphQL supports the following offline payment methods: + +Title | Code +--- | --- +Bank Transfer Payment | `banktransfer` +Cash on Delivery | `cashondelivery` +Check / Money order | `checkmo` +Credit Card (Authorize.Net) | `authorizenet_acceptjs` +No Payment Information Required | `free` +Purchase Order | `purchaseorder` + +Supported online payment methods include: + +- [Authorize.Net]({{page.baseurl}}/graphql/payment-methods/authorize-net.html) +- [Braintree]({{page.baseurl}}/graphql/payment-methods/braintree.html) +- [Braintree Vault]({{page.baseurl}}/graphql/payment-methods/braintree-vault.html) +- [PayPal Express Checkout]({{page.baseurl}}/graphql/payment-methods/paypal-express-checkout.html) +- [PayPal Payflow Link]({{page.baseurl}}/graphql/payment-methods/payflow-link.html) +- [PayPal Payflow Pro]({{page.baseurl}}/graphql/payment-methods/payflow-pro.html) +- [PayPal Payments Advanced]({{page.baseurl}}/graphql/payment-methods/payments-advanced.html) +- [PayPal Website Payments Pro Hosted Solution]({{page.baseurl}}/graphql/payment-methods/hosted-pro.html) +- [Express Checkout for other PayPal solutions]({{page.baseurl}}/graphql/payment-methods/payflow-express.html) + +## Syntax + +```graphql +mutation { + setPaymentMethodOnCart( + input: SetPaymentMethodOnCartInput + ) { + SetPaymentMethodOnCartOutput + } +} +``` + +## Example usage + +### Offline payment method + +The following example assigns the `banktransfer` payment method to the specified cart. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "rMQdWEecBZr4SVWZwj2AF6y0dNCKQ8uH" + payment_method: { + code: "banktransfer" + } + } + ) { + cart { + selected_payment_method { + code + title + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "banktransfer", + "title": "Bank Transfer Payment" + } + } + } + } +} +``` + +## Input attributes + +The top-level `SetPaymentMethodOnCartInput` object is listed first. All child objects are listed in alphabetical order. + +### SetPaymentMethodOnCartInput attributes {#SetPaymentMethodOnCartInput} + +The `SetPaymentMethodOnCartInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer’s cart +`payment_method` | [PaymentMethodInput!](#PaymentMethodInput) | An object containing the payment method code + +### PaymentMethodInput attributes {#PaymentMethodInput} + +The `PaymentMethodInput` object can contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`code` | String! | The internal name for the payment method +`purchase_order_number` | String | The purchase order number. Optional for most payment methods + +For all online payment methods, the payload must include an object that defines additional information specific to that payment method. + +## Output attributes + +The `SetPaymentMethodOnCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Required parameter "cart_id" is missing.` | The value specified in the `cart_id` argument is empty. +`Required parameter "code" for "payment_method" is missing.` | The value specified in the `code` argument is empty. +`The current user cannot perform operations on cart "XXX"` | An unauthorized user (guest) tried to set a payment method for an order on behalf of an authorized user (customer), or a customer tried to set a payment method for an order on behalf of another customer. +`The requested Payment Method is not available.` | Specified in the `payment_method` argument payment method is disabled or does not exist. +`The shipping address is missing. Set the address and try again.` | You ran `setPaymentMethodOnCart` mutation before [setShippingAddressesOnCart]({{ page.baseurl }}/graphql/mutations/set-shipping-method.html). Set a shipping address first. [GraphQL checkout tutorial]({{ page.baseurl }}/graphql/tutorials/checkout/index.html) shows the order placement sequence. diff --git a/src/guides/v2.3/graphql/mutations/set-payment-place-order.md b/src/guides/v2.3/graphql/mutations/set-payment-place-order.md new file mode 100644 index 00000000000..ff25ecb5fd7 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/set-payment-place-order.md @@ -0,0 +1,105 @@ +--- +group: graphql +title: setPaymentMethodAndPlaceOrder mutation +contributor_name: Something Digital +contributor_link: https://www.somethingdigital.com/ +redirect from: + - /guides/v2.3/graphql/reference/quote-set-payment-place-order.html +--- + +{:.bs-callout-warning} +The `setPaymentMethodAndPlaceOrder` mutation has been deprecated. Use the [setPaymentMethodOnCart]({{page.baseurl}}/graphql/mutations/set-payment-method.html) and [placeOrder]({{page.baseurl}}/graphql/mutations/place-order.html) mutations instead. You can run the two methods in the same call if your use case allows it. + +The `setPaymentMethodAndPlaceOrder` mutation sets the cart payment method and converts the cart into an order. The +mutation returns the resulting order ID. You cannot manage orders with GraphQL, because orders are part of the backend. +You can use REST or SOAP calls to manage orders to their completion. + +Perform the following actions before using the `setPaymentMethodAndPlaceOrder` mutation: + +- Create an empty cart +- Add one or more products to the cart +- Set the billing address +- Set the shipping address (non-virtual carts only) +- Set the shipping method (non-virtual carts only) +- For guest customers, assign an email to the cart + +## Syntax + +```graphql +mutation { + setPaymentMethodAndPlaceOrder( + input: SetPaymentMethodAndPlaceOrderInput + ) { + PlaceOrderOutput + } +} +``` + +## Example usage + +**Request:** + +```graphql +mutation { + setPaymentMethodAndPlaceOrder( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + payment_method: { + code: "checkmo" + } + } + ) { + order { + order_id + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodAndPlaceOrder": { + "order": { + "order_id": "000000006" + } + } + } +} +``` + +## Input attributes + +The `placeOrderInput` object must contain the following attribute: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer’s cart +`payment_method` | [PaymentMethodInput!](#PaymentMethodInput) | The payment method data for the cart + +### PaymentMethodInput attributes {#PaymentMethodInput} + +{% include graphql/quote-payment-input.md %} + +## Output attributes + +The `placeOrderOutput` object contains the `order` object, which contains the following attribute: + +Attribute | Data Type | Description +--- | --- | --- +`order_id` | String! | The unique ID that identifies the order + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Guest email for cart is missing.` | The guest attempted to place an order but did not provide an email address. See the [setGuestEmailOnCart]({{ page.baseurl }}/graphql/mutations/set-guest-email.html) mutation. +`Required parameter "cart_id" is missing` | The required `cart_id` argument contains an empty value. +`Required parameter "code" for "payment_method" is missing.` | The value specified in the `code` argument is empty. +`The current user cannot perform operations on cart "XXX"` | An unauthorized user (guest) tried to set a payment method and place an order with a customer's cart, or an authorized user (customer) tried to set a payment method and place an order with a cart of another customer. +`The shipping address is missing. Set the address and try again.` | You ran `setPaymentMethodAndPlaceOrder` mutation before [setShippingAddressesOnCart]({{ page.baseurl }}/graphql/mutations/set-shipping-method.html). Set a shipping address first. [GraphQL checkout tutorial]({{ page.baseurl }}/graphql/tutorials/checkout/index.html) shows the order placement sequence. +`The requested Payment Method is not available.` | The payment method specified in the `payment_method` argument is disabled or does not exist. +`Unable to place order: Some of the products are out of stock.` | Some of the products in a cart are out of stock. diff --git a/src/guides/v2.3/graphql/mutations/set-shipping-address.md b/src/guides/v2.3/graphql/mutations/set-shipping-address.md new file mode 100644 index 00000000000..5da81b9ea99 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/set-shipping-address.md @@ -0,0 +1,160 @@ +--- +group: graphql +title: setShippingAddressesOnCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-set-shipping-address.html +--- + +The `setShippingAddressesOnCart` mutation sets one or more shipping addresses on a specific cart. The shipping address does not need to be specified in the following circumstances: + +* The cart contains only virtual items +* When you defined the billing address, you set the `same_as_shipping` attribute to `true`. Magento assigns the same address as the shipping address. + +## Syntax + +```graphql +mutation { + setShippingAddressesOnCart( + input: SetShippingAddressesOnCartInput + ) { + SetShippingAddressesOnCartOutput + } +} +``` + +## Example usage + +**Request:** + +```graphql +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "4JQaNVJokOpFxrykGVvYrjhiNv9qt31C" + shipping_addresses: [ + { + address: { + firstname: "Bob" + lastname: "Roll" + company: "Magento" + street: ["Magento Pkwy", "Main Street"] + city: "Austin" + region: "TX" + postcode: "78758" + country_code: "US" + telephone: "8675309" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + region { + code + label + } + postcode + telephone + country { + code + label + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setShippingAddressesOnCart": { + "cart": { + "shipping_addresses": [ + { + "firstname": "Bob", + "lastname": "Roll", + "company": "Magento", + "street": [ + "Magento Pkwy", + "Main Street" + ], + "city": "Austin", + "region": { + "code": "TX", + "label": "Texas" + }, + "postcode": "78758", + "telephone": "8675309", + "country": { + "code": "US", + "label": "US" + } + } + ] + } + } + } +} +``` + +## Input attributes + +The top-level `SetShippingAddressesOnCartInput` object is listed first. All child objects are listed in alphabetical order. + +### SetShippingAddressesOnCartInput object {#SetShippingAddressesOnCartInput} + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`shipping_addresses` | [ShippingAddressInput!](#ShippingAddressInput) | The shipping address for a specific cart + +### CartAddressInput object {#CartAddressInputShip} + +{% include graphql/cart-address-input.md %} + +### ShippingAddressInput object {#ShippingAddressInput} + +Attribute | Data Type | Description +--- | --- | --- +`address` | [CartAddressInput](#CartAddressInputShip) | The shipping address for the cart +`customer_address_id` | Int | The unique ID that identifies the customer's address +`customer_notes` | String | Text provided by the customer + +## Output attributes + +The `SetShippingAddressOnCartOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a address with ID "XXX"` | The specified `input`.`shipping_addresses`.`customer_address_id` value does not exist in the `customer_address_entity` database table. +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` database table. +`Current customer does not have permission to address with ID "XXX"` | The specified address ID in the `input`.`shipping_addresses`.`customer_address_id` argument belongs to another customer. +`Required parameter "cart_id" is missing.` | The value specified in the `cart_id` argument is empty. +`Required parameter "shipping_addresses" is missing` | The `shipping_addresses` argument is empty. +`The Cart includes virtual product(s) only, so a shipping address is not used.` | You do not need to specify a shipping address because virtual products are not delivered. +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. +`The current user cannot perform operations on cart "XXX"` | An unauthorized user (guest) tried to update a shipping address of a customer's cart, or an authorized user (customer) tried to update the shipping address of another customer's cart. +`The shipping address cannot contain "customer_address_id" and "address" at the same time.` | Specify either the ID of the existing customer's address in the `input`.`shipping_addresses`.`customer_address_id` argument or a new customer's address in the `input`.`shipping_addresses`.`address` argument (but not both). +`You cannot specify multiple shipping addresses.` | You cannot specify more than one customer's address in the `input`.`shipping_addresses`.`address` argument. diff --git a/src/guides/v2.3/graphql/mutations/set-shipping-method.md b/src/guides/v2.3/graphql/mutations/set-shipping-method.md new file mode 100644 index 00000000000..03192bdc038 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/set-shipping-method.md @@ -0,0 +1,140 @@ +--- +group: graphql +title: setShippingMethodsOnCart mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-shipping-method.html +--- + +The `setShippingMethodsOnCart` mutation sets one or more shipping methods on a cart. By default, Magento GraphQL supports the following shipping methods: + +Label | Carrier code | Method code +--- | --- | --- +DHL | dhl | Varies +Federal Express | fedex | Varies +Flat Rate | flatrate | flatrate +Free Shipping | freeshipping | freeshipping +Best Way | tablerate | bestway +United Parcel Service | ups | Varies +United States Postal Service | usps | Varies + +## Syntax + +```graphql +mutation { + setShippingMethodsOnCart( + input: setShippingMethodsOnCartInput + ) { + setShippingMethodsOnCartOutput + } +} +``` + +## Example usage + +The following example sets the shipping method to Best Way. + +**Request:** + +```graphql +mutation { + setShippingMethodsOnCart( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG", + shipping_methods: [ + { + carrier_code: "tablerate" + method_code: "bestway" + } + ] + } + ) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + carrier_title + method_code + method_title + amount { + value + currency + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setShippingMethodsOnCart": { + "cart": { + "shipping_addresses": [ + { + "selected_shipping_method": { + "carrier_code": "tablerate", + "carrier_title": "Best Way", + "method_code": "bestway", + "method_title": "Table Rate", + "amount": { + "value": 0, + "currency": "USD" + } + } + } + ] + } + } + } +} +``` + +## Input attributes + +The top-level `setShippingMethodsOnCartInput` object is listed first. All child objects are listed in alphabetical order. + +### setShippingMethodsOnCartInput object {#setShippingMethodsOnCartInput} + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`shipping_methods` | [ShippingMethodInput!](#ShippingMethodInput) | The shipping address for a specific cart + +### ShippingMethodInput object {#ShippingMethodInput} + +Attribute | Data Type | Description +--- | --- | --- +`carrier_code` | String! | A string that identifies a commercial carrier or an offline shipping method +`method_code` | String! | A string that indicates which service a commercial carrier will use to ship items. For offline shipping methods, this value is similar to the label displayed on the checkout page + +## Output attributes + +The `ShippingMethodOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[ Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` table. +`Carrier with such method not found: carrier_code, method_code` | A specified carrier method was not found, or it is not applicable for the defined shipping address. +`Required parameter "cart_id" is missing` | The value specified in the `cart_id` argument is empty. +`Required parameter "carrier_code" is missing.` | The value specified in the `shipping_methods`.`carrier_code` argument is empty. +`Required parameter "method_code" is missing.` | The value specified in the `shipping_methods`.`method_code` argument is empty. +`Required parameter "shipping_methods" is missing` | The value specified in the `shipping_methods` argument is empty. +`The current user cannot perform operations on cart "XXX"` | An unauthorized user (guest) tried to set a shipping method for an order on behalf of an authorized user (customer), or a customer tried to set a shipping method for an order on behalf of another customer. +`The shipping method can't be set for an empty cart. Add an item to cart and try again.` | The shipping method cannot be set for an empty cart. +`You cannot specify multiple shipping methods.` | You can set only one shipping method for an order. diff --git a/src/guides/v2.3/graphql/mutations/update-cart-items.md b/src/guides/v2.3/graphql/mutations/update-cart-items.md new file mode 100644 index 00000000000..9c24356477a --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/update-cart-items.md @@ -0,0 +1,150 @@ +--- +group: graphql +title: updateCartItems mutation +redirect from: + - /guides/v2.3/graphql/reference/quote-update-cart-items.html +--- + +The `updateCartItems` mutation allows you to replace the current quantity of one or more cart items with the specified quantities. It does not perform calculations to determine the quantity of cart items. + +{:.bs-callout-info} +Setting the quantity to `0` removes an item from the cart. + +## Syntax + +```graphql +mutation { + updateCartItems( + input: UpdateCartItemsInput + ) { + UpdateCartItemsOutput + } +} +``` + +## Example usage + +The following example changes the quantity of cart item `13`. The new quantity is `3`. + +**Request:** + +```graphql +mutation { + updateCartItems( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG", + cart_items: [ + { + cart_item_id: 13 + quantity: 3 + } + ] + } + ) { + cart { + items { + id + product { + name + } + quantity + } + prices { + grand_total{ + value + currency + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "updateCartItems": { + "cart": { + "items": [ + { + "id": "13", + "product": { + "name": "Strive Shoulder Pack" + }, + "quantity": 3 + }, + { + "id": "14", + "product": { + "name": "Affirm Water Bottle " + }, + "quantity": 1 + } + ], + "prices": { + "grand_total": { + "value": 103, + "currency": "USD" + } + } + } + } + } +} +``` + +## Input attributes + +The `UpdateCartItemsInput` object is listed first. All child objects are listed in alphabetical order. + +### UpdateCartItemsInput object {#UpdateCartItemsInput} + +The `UpdateCartItemsInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart +`cart_items` | [CartItemUpdateInput!](#CartItemUpdateInput) | Contains the cart item IDs and quantity of each item + +### CartItemUpdateInput object {#CartItemUpdateInput} + +The `CartItemUpdateInput` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`cart_item_id` | Int! | The unique ID assigned when a customer places an item in the cart +`customizable_options` | [CustomizableOptionInput!] | An array that defines customizable options for the product +`quantity` | Float | The new quantity of the item. A value of `0` removes the item from the cart + +### CustomizableOptionInput object {#CustomizableOptionInputSimple} + +The `CustomizableOptionInput` object must contain the following attributes: + +{% include graphql/customizable-option-input.md %} + +## Output attributes + +The `UpdateCartItemsOutput` object contains the `Cart` object. + +Attribute | Data Type | Description +--- | --- | --- +`cart` |[Cart!](#CartObject) | Describes the contents of the specified shopping cart + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. + +## Errors + +Error | Description +--- | --- +`Could not find cart item with id: XXX` | The specified `input`.`cart_items`.`cart_item_id` value does not exist in the `quote_item` database table. +`Could not find a cart with ID "XXX"` | The specified `cart_id` value does not exist in the `quote_id_mask` database table. +`Required parameter "cart_id" is missing.` | The value specified in the `cart_id` argument is empty. +`Required parameter "cart_items" is missing.` | The `cart_items` argument is empty, or its value is specified as a non-array value. +`Required parameter "quantity" for "cart_items" is missing.` | The required `input`.`cart_items`.`quantity` argument must be specified. +`The current user cannot perform operations on cart "XXX"` | An unauthorized user (guest) tried to update a customer's cart, or an authorized user (customer) tried to update the cart of another customer. diff --git a/src/guides/v2.3/graphql/mutations/update-customer-address.md b/src/guides/v2.3/graphql/mutations/update-customer-address.md new file mode 100644 index 00000000000..dfa11a30dd3 --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/update-customer-address.md @@ -0,0 +1,95 @@ +--- +group: graphql +title: updateCustomerAddress mutation +--- + +Use the `updateCustomerAddress` mutation to update the customer's address. + +To return or modify information about a customer, we recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +```graphql +mutation { + updateCustomerAddress( + id: Int! + input: CustomerAddressInput + ) { + CustomerAddress + } +} +``` + +## Example usage + +The following call updates the customer's city and postcode. + +**Request:** + +```graphql +mutation { + updateCustomerAddress( + id:3 + input: { + city: "New City" + postcode: "55555" + } + ) { + id + city + postcode + } +} +``` + +**Response:** + +```json +{ + "data": { + "updateCustomerAddress": { + "id": 3, + "city": "New City", + "postcode": 55555 + } + } +} +``` + +## Input attributes + +The `updateCustomerAddress` object contains the following inputs: + +Attribute | Data Type | Description +--- | --- | --- +`id` | Int! | The ID assigned to the address object +`CustomerAddressInput` | [CustomerAddress](#customerAddressInput)| An array containing the customer’s shipping and billing addresses + +{% include graphql/customer-address-input.md %} + +## Output attributes + +The `updateCustomerAddress` mutation returns the `CustomerAddress` object. + +{% include graphql/customer-address-output.md %} + +## Errors + +Error | Description +--- | --- +`Address "id" value should be specified` | The `id` argument is zero. +`Could not find a address with ID "XXX"` | The customer address specified in the `id` argument does not exist. +`Current customer does not have permission to address with ID "XXX"` | The customer tries to update the address of another customer. +`Field "updateCustomerAddress" argument "id" of type "Int!" is required but not provided.` | The `id` argument was omitted. +`Field "updateCustomerAddress" argument "id" requires type Int!, found "XXX".` | The specified `id` argument value has the wrong type. +`"input" value must be specified` | The `input` argument was omitted or was specified but is empty. +`Syntax Error: Expected Name, found )` | The `id` and `input` arguments are omitted. +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. + +## Related topics + +* [customer query]({{page.baseurl}}/graphql/queries/customer.html) +* [createCustomer mutation]({{page.baseurl}}/graphql/mutations/create-customer.html) +* [updateCustomer mutation]({{page.baseurl}}/graphql/mutations/update-customer.html) +* [createCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/create-customer-address.html) +* [deleteCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/delete-customer-address.html) diff --git a/src/guides/v2.3/graphql/mutations/update-customer.md b/src/guides/v2.3/graphql/mutations/update-customer.md new file mode 100644 index 00000000000..9363e24438e --- /dev/null +++ b/src/guides/v2.3/graphql/mutations/update-customer.md @@ -0,0 +1,92 @@ +--- +group: graphql +title: updateCustomer mutation +--- + +Use the `updateCustomer` mutation to update the customer's personal information. + +To return or modify information about a customer, we recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +```graphql +mutation { + updateCustomer( + input: CustomerInput! + ) { + CustomerOutput + } +} +``` + +## Example usage + +The following call updates the first name and email address for a specific customer. + +**Request:** + +```graphql +mutation { + updateCustomer( + input: { + firstname: "Rob" + email: "robloblaw@example.com" + } + ) { + customer { + firstname + email + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "updateCustomer": { + "customer": { + "firstname": "Rob", + "email": "robloblaw@example.com" + } + } + } +} +``` + +## Input attributes + +The following table lists the attributes you can use as input for the `updateCustomer` mutation. The [Customer attributes]({{page.baseurl}}/graphql/queries/customer.html#customerAttributes) table lists the attributes Magento returns. + +{% include graphql/create-customer.md %} + +## Output attributes + +The `updateCustomer` mutation returns the `CustomerOutput` object. + +{% include graphql/customer-output.md %} + +## Errors + +Error | Description +--- | --- +`"input" value should be specified` | The `input` argument is empty. +`"Email" is not a valid email address.` | The value provided in the `input`.`email` argument has an invalid format. +`A customer with the same email address already exists in an associated website.` | You cannot set a new email to a current customer, because another user has the specified email. +`Invalid date` | An incorrect value was provided in the `date_of_birth` argument. +`Invalid login or password.` | The value specified in the `password` argument is incorrect. +`Provide the current "password" to change "email".` | To change an email address, specify the correct customer password in the `password` argument. +`Required parameters are missing: First Name` | The customer first name cannot have an empty value. +`Required parameters are missing: Last Name` | The customer last name cannot have an empty value. +`The account is locked.` | You cannot modify a locked customer account. +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. + +## Related topics + +* [customer query]({{page.baseurl}}/graphql/queries/customer.html) +* [createCustomer mutation]({{page.baseurl}}/graphql/mutations/create-customer.html) +* [createCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/create-customer-address.html) +* [updateCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/update-customer-address.html) +* [deleteCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/delete-customer-address.html) diff --git a/src/guides/v2.3/graphql/payment-methods/authorize-net.md b/src/guides/v2.3/graphql/payment-methods/authorize-net.md new file mode 100644 index 00000000000..b587937aeec --- /dev/null +++ b/src/guides/v2.3/graphql/payment-methods/authorize-net.md @@ -0,0 +1,87 @@ +--- +group: graphql +title: Authorize.Net payment method +--- + +{:.bs-callout-warning} +The Authorize.Net payment method has been deprecated for Magento 2.3.5 and will be removed in Magento 2.4.0. + +Accept.js is a JavaScript-based solution for sending secure payment data directly to Authorize.Net. The Accept JavaScript library intercepts the payment data before it is passed to Magento and submits it directly to Authorize.Net, which replaces it with a one-time-use token, or payment nonce. This payment nonce, which is returned by the JavaScript library, is used in place of payment data. + +## Authorize.Net workflow + +The following diagram shows the workflow for placing an order when Authorize.Net is the selected payment method. + +![Authorize.Net sequence diagram]({{site.baseurl}}/common/images/graphql/authorize-net.svg) + +1. The customer clicks on the **Place order** button. The embedded `Accept.js` library captures the payment data and submits it directly to Authorize.Net. + +1. Authorize.Net returns a payment nonce and order details. The client's browser then posts the nonce to the Magento server along with all the other order information. The payment nonce expires after 24 hours. + +1. The client uses the [`setPaymentMethodOnCart`]({{page.baseurl}}/graphql/mutations/set-payment-method.html) mutation to send the payment nonce and the last four digits of the card to Magento. + +1. Magento returns a `Cart` object. + +1. The client runs the [`placeOrder`]({{page.baseurl}}/graphql/mutations/place-order.html) mutation, which creates an order in Magento and begins the authorization process. + +1. Magento sends a transaction request that includes the payment nonce. The nonce replaces the payment details provided in standard Authorize.Net API calls. + +1. Authorize.Net sends a transaction response. + +1. Magento creates an order and sends an order ID in response to the `placeOrder` mutation. + +## Additional payment information + +When you set the payment method to `authorizenet_acceptjs` in the [`setPaymentMethodOnCart`]({{page.baseurl}}/graphql/mutations/set-payment-method.html) mutation, the payload must contain an `authorizenet_acceptjs` object. + +Attribute | Data Type | Description +--- | --- | --- +`cc_last_4` | Int! | The last four digits of the credit or debit card +`opaque_data_descriptor` | String! | Authorize.Net's description of the transaction request +`opaque_data_value` | String! | The nonce returned by Authorize.Net + +### Example usage + +The following example assigns the `authorizenet_acceptjs` payment method to the specified cart. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart(input: { + cart_id: "lvdqOLzryManseE2artECZuPClxFgG1o" + payment_method: { + code: "authorizenet_acceptjs" + authorizenet_acceptjs: { + cc_last_4: 1111 + opaque_data_descriptor: "COMMON.ACCEPT.INAPP.PAYMENT" + opaque_data_value: "" + } + } + }) { + cart { + selected_payment_method { + code + title + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "authorizenet_acceptjs", + "title": "Credit Card (Authorize.Net)" + } + } + } + } +} +``` diff --git a/src/guides/v2.3/graphql/payment-methods/braintree-vault.md b/src/guides/v2.3/graphql/payment-methods/braintree-vault.md new file mode 100644 index 00000000000..b696bedcd02 --- /dev/null +++ b/src/guides/v2.3/graphql/payment-methods/braintree-vault.md @@ -0,0 +1,91 @@ +--- +group: graphql +title: Braintree Vault payment method +contributor_name: Something Digital +contributor_link: https://www.somethingdigital.com/ +--- + +Braintree Vault is a payment gateway that processes debit and credit card payments from the Magento_Vault. + +## Braintree Vault workflow + +The following diagram shows the workflow for placing an order when Braintree Vault is the selected payment method. + +![Braintree sequence diagram]({{site.baseurl}}/common/images/graphql/braintree-vault.svg) + +1. Use the [`customerPaymentTokens`]({{page.baseurl}}/graphql/queries/customer-payment-tokens.html) query to retrieve + the payment tokens the customer has stored in the vault. + +1. Magento returns an array of payment tokens. + +1. The client renders the token information, and the customer selects a payment method. + + When the customer selects a stored payment method, the PWA uses the [`setPaymentMethodOnCart`]({{page.baseurl}}/graphql/mutations/set-payment-method.html) mutation to set the payment method to [`braintree_cc_vault`](#braintree_cc_vault-object). The vaulted public hash is passed with other optional properties in the `braintree_cc_vault`. + +1. Magento returns a `Cart` object. + +1. The client runs the [`placeOrder`]({{page.baseurl}}/graphql/mutations/place-order.html) mutation. + +1. Magento sends an authorization request to the gateway. + +1. The gateway sends the response to Magento. + +1. Magento creates an order and sends an order ID in response to the `placeOrder` mutation. + +## `setPaymentMethodOnCart` mutation + +When you set the payment method to Braintree in the [`setPaymentMethodOnCart`]({{page.baseurl}}/graphql/mutations/set-payment-method.html) +mutation, the `payment_method` object must contain a [`braintree_cc_vault`](#braintree_cc_vault-object) object. + +### braintree_cc_vault object + +The `braintree_cc_vault` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`public_hash` | String! | Required input for Magento_Vault public hash for the selected stored payment method +`device_data` | String | Optional. JSON-encoded device data for Kount integration + +### Example Usage + +The following example shows the `setPaymentMethodOnCart` mutation constructed for the Braintree Vault payment method. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + payment_method: { + code: "braintree_cc_vault" + braintree_cc_vault: { public_hash: "fake-public-hash" } + } + } + ) { + cart { + selected_payment_method { + code + title + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "braintree_cc_vault" + "title": "Stored Cards" + } + } + } + } +} +``` diff --git a/src/guides/v2.3/graphql/payment-methods/braintree.md b/src/guides/v2.3/graphql/payment-methods/braintree.md new file mode 100644 index 00000000000..1cccd5c8589 --- /dev/null +++ b/src/guides/v2.3/graphql/payment-methods/braintree.md @@ -0,0 +1,100 @@ +--- +group: graphql +title: Braintree payment method +contributor_name: Something Digital +contributor_link: https://www.somethingdigital.com/ +--- + +Braintree is a payment gateway that processes debit and credit card payments. + +## Braintree workflow + +The following diagram shows the workflow for placing an order when Braintree is the selected payment method. + +![Braintree sequence diagram]({{site.baseurl}}/common/images/graphql/braintree.svg) + +1. The PWA client calls the [`createBraintreeClientToken`]({{page.baseurl}}/graphql/mutations/create-braintree-client-token.html) mutation to generate the client token. + +1. Magento forwards the request to Braintree. + +1. Braintree returns the token to Magento. + +1. Magento forwards the token to the client. + +1. The PWA client uses the token to initialize the [Braintree hosted fields](https://developers.braintreepayments.com/guides/hosted-fields/overview/javascript/v3). These fields collect and tokenize payment information via a secure iframe. This process occurs over several steps. + + - On the checkout page, the customer selects **Credit Card** as the payment method and enters payment information using the Braintree hosted fields. Then the customer clicks **Place Order**. + + - The client requests the Braintree SDK tokenize the user-input payment information. + +1. The Braintree SDK submits the payment information to Braintree client-side and returns a [payment token](https://braintree.github.io/braintree-web/3.46.0/HostedFields.html#tokenize) (nonce) to the client. + +1. The client extracts the payment nonce from the [Tokenized Payload](https://braintree.github.io/braintree-web/3.46.0/HostedFields.html#~tokenizePayload). + + The client uses the [`setPaymentMethodOnCart`]({{page.baseurl}}/graphql/mutations/set-payment-method.html) mutation to set the payment method to `braintree`. The payment method nonce is passed with other required and optional properties in the [`braintree`](#braintree-object) object. + +1. Magento returns a `Cart` object. + +1. The client uses the [`placeOrder`]({{page.baseurl}}/graphql/mutations/place-order.html) mutation. + +1. Magento sends an authorization request to Braintree. + +1. Braintree sends the response to Magento. + +1. Magento creates an order and sends an order ID in response to the `placeOrder` mutation. + +## `setPaymentMethodOnCart` mutation + +When you set the payment method to Braintree in the [`setPaymentMethodOnCart`]({{page.baseurl}}/graphql/mutations/set-payment-method.html) mutation, the `payment_method` object must contain a `braintree` object. + +### braintree object + +The `braintree` object must contain the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`payment_method_nonce` | String! | The one-time payment token generated by Braintree payment gateway based on card details. Required field for sale transactions +`is_active_payment_token_enabler` | Boolean! | States whether a customer-entered credit/debit card should be tokenized for later usage. Required only if Vault is enabled for Braintree payment integration +`device_data` | String | Optional. Contains a fingerprint provided by the Braintree JS SDK. It should be sent with sale transaction details to the Braintree payment gateway. Specify a value only if Kount (advanced fraud protection) is enabled for Braintree payment integration + +## Example Usage + +The following example shows the `setPaymentMethodOnCart` mutation constructed for the Braintree payment method. + +**Request:** + +```text +mutation { + setPaymentMethodOnCart(input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + payment_method: { + code: "braintree" + braintree: { + payment_method_nonce: "fake-nonce" + is_active_payment_token_enabler: false + } + } + }) { + cart { + selected_payment_method { + code + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "braintree" + } + } + } + } +} +``` diff --git a/src/guides/v2.3/graphql/payment-methods/hosted-pro.md b/src/guides/v2.3/graphql/payment-methods/hosted-pro.md new file mode 100644 index 00000000000..2890376c9bb --- /dev/null +++ b/src/guides/v2.3/graphql/payment-methods/hosted-pro.md @@ -0,0 +1,69 @@ +--- +group: graphql +title: PayPal Website Payments Pro Hosted Solution payment method +--- + +PayPal's [Website Payments Pro Hosted Solution](https://developer.paypal.com/docs/classic/paypal-payments-pro/integration-guide/) allows merchants to accept credit cards, debit cards, and PayPal payments directly on their websites. The merchant must be based in the United Kingdom to create a new integration with this payment method. PayPal continues to support merchants with existing integrations outside the UK. + +This payment method is applicable to Direct Payment and Express Checkout implementations of the Website Payments Pro Hosted Solution. + +PayPal's product name for this payment method varies from country to country. [PayPal Website Payments +Pro Hosted Solution Integration Guide](https://www.paypalobjects.com/webstatic/en_GB/developer/docs/pdf/hostedsolution_uk.pdf) provides more information. + +## Website Payments Pro Hosted Solution workflow + +The following diagram shows the workflow for placing an order when Website Payments Pro Hosted Solution is the selected payment method. + +![PayPal Website Payments Pro Hosted Solution sequence diagram]({{site.baseurl}}/common/images/graphql/paypal-hosted-pro.svg) + +{% include graphql/payment-methods/hosted-pro-workflow.md %} + +## `setPaymentMethodOnCart` mutation + +When you set the payment method for a Website Payments Pro Hosted Solution, you must set the `code` attribute to `hosted_pro`. In addition, the payload must contain a `hosted_pro` object, which defines the following attributes: + +{% include graphql/payment-methods/hosted-pro-attributes.md %} + +### Example usage + +The following example shows the `setPaymentMethodOnCart` mutation constructed for the Website Payments Pro Hosted Solution payment method. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart(input: { + cart_id: "H87OmEkvusP7ZPkd2634pQFxY4dKI3a4" + payment_method: { + code: "hosted_pro" + hosted_pro: { + cancel_url: "paypal/hostedpro/cancel" + return_url: "paypal/hostedpro/return" + } + } + }) + { + cart { + selected_payment_method { + code + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "hosted_pro", + } + } + } + } +} +``` diff --git a/src/guides/v2.3/graphql/payment-methods/payflow-express.md b/src/guides/v2.3/graphql/payment-methods/payflow-express.md new file mode 100644 index 00000000000..4efada184be --- /dev/null +++ b/src/guides/v2.3/graphql/payment-methods/payflow-express.md @@ -0,0 +1,80 @@ +--- +group: graphql +title: Express Checkout for other PayPal solutions +--- + +Set the payment method code to `payflow_express` to process Express Checkout transactions when the payment method is set to any of the following: + +- Payflow Link +- Payflow Pro +- Payments Advanced +- Payments Pro + +PayPal Express Checkout enables customers to pay by credit card or from the security of their personal PayPal accounts. During checkout, the customer is redirected to the secure PayPal site to complete the payment information. The customer is then returned to the store to complete the remainder of the checkout process. + +From a GraphQL integration standpoint, this payment method is identical to the [PayPal Express Checkout]({{page.baseurl}}/graphql/payment-methods/payflow-link.html) payment method, with the exception that in the `setPaymentMethodOnCart` mutation, the payment method `code` is set to `payflow_express`. + +## PayPal Express Checkout workflow + +The following diagram shows the workflow for placing an order when `payflow_express` is the specified payment method. + +![PayPal Express Checkout sequence diagram]({{site.baseurl}}/common/images/graphql/paypal-express-checkout.svg) + +The following steps describe the flow of calls required to complete a typical PayPal Express Checkout authorization. A successful purchase requires that you send three mutations to PayPal, and the buyer must approve the purchase by logging in to PayPal. + +{% include graphql/payment-methods/paypal-express-checkout-workflow.md %} + +## `setPaymentMethodOnCart` mutation + +When you set the payment method to one of the Express Checkout payment solutions discussed in this topic, you must set the `code` attribute to `payflow_express`. In addition, the payload must contain a `payflow_express` object, which defines the following attributes: + +{% include graphql/payment-methods/paypal-express-checkout-attributes.md %} + +### Example usage + +The following example shows the `setPaymentMethodOnCart` mutation with the `code` set to `payflow_express`. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart(input: { + cart_id: "rMQdWEecBZr4SVWZwj2AF6y0dNCKQ8uH" + payment_method: { + code: "payflow_express" + payflow_express: { + payer_id: "" + token: "" + } + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "payflow_express", + } + } + } + } +} +``` + +## Related topics + +- [`createPaypalExpressToken` mutation]({{page.baseurl}}/graphql/mutations/create-paypal-express-token.html) +- [`placeOrder` mutation]({{page.baseurl}}/graphql/mutations/place-order.html) +- [`setPaymentMethodOnCart` mutation]({{page.baseurl}}/graphql/mutations/set-payment-method.html) diff --git a/src/guides/v2.3/graphql/payment-methods/payflow-link.md b/src/guides/v2.3/graphql/payment-methods/payflow-link.md new file mode 100644 index 00000000000..04a34434c97 --- /dev/null +++ b/src/guides/v2.3/graphql/payment-methods/payflow-link.md @@ -0,0 +1,68 @@ +--- +group: graphql +title: PayPal Payflow Link payment method +--- + +PayPal [PayFlow Link](https://developer.paypal.com/docs/classic/payflow/integration-guide/) is available for merchants in the United States and Canada only. Customers are not required to have a personal PayPal account. Instead, customers enter their credit card information in a form that is hosted by PayPal. + +The Payflow gateway uses a secure token to send non-credit card transaction data to the Payflow server for storage in a way that cannot be intercepted and manipulated maliciously. This token secures the data for a one-time transaction and is valid for 30 minutes. When the PWA client runs the `placeOrder` mutation, Magento requests a secure token. The Payflow server returns the token as a string of up to 32 alphanumeric characters. + +## Payflow Link workflow + +The following diagram shows the workflow for placing an order when Payflow Link is the selected payment method. + +![PayPal Payflow Link sequence diagram]({{site.baseurl}}/common/images/graphql/paypal-payflow-link.svg) + +{% include graphql/payment-methods/payflow-link-workflow.md %} + +## Additional Payment information + +When you set the payment method to Payflow Link in the [`setPaymentMethodOnCart`]({{page.baseurl}}/graphql/mutations/set-payment-method.html) mutation, the `payment_method` object must contain a `payflow_link` object, which defines the following objects: + +{% include graphql/payment-methods/payflow-link-attributes.md %} + +### Example usage + +The following example shows the `setPaymentMethodOnCart` mutation constructed for the Payflow Link payment method. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart(input: { + payment_method: { + code: "payflow_link" + payflow_link: { + return_url: "paypal/action/return.html" + error_url: "paypal/action/error.html" + cancel_url: "paypal/action/cancel.html" + } + } + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + }) { + cart { + selected_payment_method { + code + title + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "payflow_link", + "title": "PayPal Payflow Link" + } + } + } + } +} +``` diff --git a/src/guides/v2.3/graphql/payment-methods/payflow-pro.md b/src/guides/v2.3/graphql/payment-methods/payflow-pro.md new file mode 100644 index 00000000000..95b555c7d20 --- /dev/null +++ b/src/guides/v2.3/graphql/payment-methods/payflow-pro.md @@ -0,0 +1,77 @@ +--- +group: graphql +title: PayPal Payflow Pro payment method +--- + +Payflow Pro is a payment gateway that processes debit and credit card payments. It is available for customers of the United States, Canada, Australia, and New Zealand. + +Other PayPal solutions have the same GraphQL workflow as Payflow Pro. The information in this topic also applies to the following PayPal solution: + +- Payments Pro + +If Payflow Pro has been configured to implement Express Checkout, use the [PayPal Express Checkout for Payflow payment method]({{page.baseurl}}/graphql/payment-methods/payflow-express.html) instead. + +## Payflow Pro workflow + +The following diagram shows the workflow for placing an order when Payflow Pro is the selected payment method. + +![PayPal Payflow Pro sequence diagram]({{site.baseurl}}/common/images/graphql/paypal-payflow-pro.svg) + +{% include graphql/payment-methods/payflow-pro-workflow.md %} + +## Additional Payment information + +When you set the payment method to Payflow Pro in the [`setPaymentMethodOnCart`]({{page.baseurl}}/graphql/mutations/set-payment-method.html) mutation, the `payment_method` object must contain a `payflowpro` object and a `CreditCardDetailsInput` object. + +{% include graphql/payment-methods/payflow-pro-attributes.md %} + +### Example usage + +The following example shows the `setPaymentMethodOnCart` mutation constructed for the Payflow Pro payment method. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + payment_method: { + code: "payflowpro" + payflowpro: { + cc_details: { + cc_exp_month: 12 + cc_exp_year: 2021 + cc_last_4: 1111 + cc_type: "VI" + } + } + } + } + ) { + cart { + selected_payment_method { + code + title + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "payflowpro" + "title": "Payflow Pro" + } + } + } + } +} +``` diff --git a/src/guides/v2.3/graphql/payment-methods/payments-advanced.md b/src/guides/v2.3/graphql/payment-methods/payments-advanced.md new file mode 100644 index 00000000000..6970d3fbd2e --- /dev/null +++ b/src/guides/v2.3/graphql/payment-methods/payments-advanced.md @@ -0,0 +1,72 @@ +--- +group: graphql +title: PayPal Payments Advanced payment method +--- + +The PayPal Payments Advanced payment solution allows merchants to enable their online stores to collect payments directly via credit card or from the PayPal Credit service. If Payments Advanced has been configured to implement Express Checkout, use the [PayPal Express Checkout for Payflow payment method]({{page.baseurl}}/graphql/payment-methods/payflow-express.html) instead. + +From a GraphQL integration standpoint, PayPal Payments Advanced payment method is identical to the PayPal [Payflow Link]({{page.baseurl}}/graphql/payment-methods/payflow-link.html) payment method, with the exception of the payment method `code`. The PayPal [Payments Advanced documentation](https://developer.paypal.com/docs/classic/products/paypal-payments-advanced/) describes other ways in which the payment methods differ. + +PayPal Payments Advanced is available in the US and Canada only. + +## PayPal Payments Advanced workflow + +The following diagram shows the workflow for placing an order when Payments Advanced is the selected payment method. + +![PayPal Payments Advanced sequence diagram]({{site.baseurl}}/common/images/graphql/paypal-payflow-link.svg) + +{% include graphql/payment-methods/payflow-link-workflow.md %} + +## Additional Payment information + +## `setPaymentMethodOnCart` mutation + +When you set the payment method to PayPal Payments Advanced, you must set the `code` attribute to `payflow_advanced`. In addition, the payload must contain a `payflow_link` object, which defines the following attributes: + +{% include graphql/payment-methods/payflow-link-attributes.md %} + +### Example usage + +The following example shows the `setPaymentMethodOnCart` mutation constructed for the Payments Advanced payment method. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart(input: { + payment_method: { + code: "payflow_advanced" + payflow_link: { + return_url: "paypal/action/return.html" + error_url: "paypal/action/error.html" + cancel_url: "paypal/action/cancel.html" + } + } + cart_id: "IeTUiU0oCXjm0uRqGCOuhQ2AuQatogjG" + }) { + cart { + selected_payment_method { + code + title + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "payflow_advanced", + "title": "Credit Card" + } + } + } + } +} +``` diff --git a/src/guides/v2.3/graphql/payment-methods/paypal-express-checkout.md b/src/guides/v2.3/graphql/payment-methods/paypal-express-checkout.md new file mode 100644 index 00000000000..fdd9e85b30a --- /dev/null +++ b/src/guides/v2.3/graphql/payment-methods/paypal-express-checkout.md @@ -0,0 +1,78 @@ +--- +group: graphql +title: PayPal Express Checkout payment method +--- + +The PayPal Express Checkout payment method enables customers to pay by credit card or from the security of their personal PayPal accounts. During checkout, the customer is redirected to the secure PayPal site to complete the payment information. The customer is then returned to the store to complete the remainder of the checkout process. + +Some alternate PayPal solutions have the same GraphQL workflow when Express Checkout is enabled. The information in this topic also applies to the following PayPal solutions: + +- Payments Standard +- Website Payments Standard + +## PayPal Express Checkout workflow + +The following diagram shows the workflow for placing an order when PayPal Express Checkout is the selected payment method. + +![PayPal Express Checkout sequence diagram]({{site.baseurl}}/common/images/graphql/paypal-express-checkout.svg) + +The following steps describe the flow of calls required to complete a typical PayPal Express Checkout authorization. A successful purchase requires that you send three mutations to PayPal, and the buyer must approve the purchase by logging in to PayPal. + +{% include graphql/payment-methods/paypal-express-checkout-workflow.md %} + +## `setPaymentMethodOnCart` mutation + +When you set the payment method to Express Checkout, you must set the `code` attribute to `paypal_express`. In addition, the payload must contain a `paypal_express` object, which defines the following attributes: + +{% include graphql/payment-methods/paypal-express-checkout-attributes.md %} + +### Example usage + +The following example shows the `setPaymentMethodOnCart` mutation constructed for the PayPal Express payment method. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart(input: { + cart_id: "rMQdWEecBZr4SVWZwj2AF6y0dNCKQ8uH" + payment_method: { + code: "paypal_express" + paypal_express: { + payer_id: "" + token: "" + } + } + }) { + cart { + selected_payment_method { + code + title + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "paypal_express", + "title": "PayPal Express Checkout" + } + } + } + } +} +``` + +## Related topics + +- [`createPaypalExpressToken` mutation]({{page.baseurl}}/graphql/mutations/create-paypal-express-token.html) +- [`placeOrder` mutation]({{page.baseurl}}/graphql/mutations/place-order.html) +- [`setPaymentMethodOnCart` mutation]({{page.baseurl}}/graphql/mutations/set-payment-method.html) diff --git a/src/guides/v2.3/graphql/protected-mutations.md b/src/guides/v2.3/graphql/protected-mutations.md new file mode 100644 index 00000000000..6a8d9f2e3aa --- /dev/null +++ b/src/guides/v2.3/graphql/protected-mutations.md @@ -0,0 +1,56 @@ +--- +group: graphql +title: Protected mutations +--- + +If CAPTCHA or reCAPTCHA is enabled on pages requiring shopper input, then in most cases, the corresponding mutations that send requests to the Magento server must include an HTTP header that contains a value entered by the shopper (for CAPTCHA) or generated by the Google API (for reCAPTCHA). However, if you specify an integration authorization token in the header of the mutation, then you do not supply a header specific to CAPTCHA or reCAPTCHA. + +The HTTP `X-Captcha` and `X-ReCaptcha` headers: + +* Cannot be received by an automated script or a non-UI API call. They are captured and returned by the UI Web form only. +* Are optional in protected mutation API calls that provide **_integration authorization_** tokens only. They cannot be skipped when you provide an Admin or Bearer token. + +## CAPTCHA + +The following table lists the forms that can be configured to require CAPTCHA. Go to **Stores** > **Configuration** > **Customers** > **Customer Configuration** > **CAPTCHA** > **Forms** to enable or disable CAPTCHA on these forms. + +The mutation that corresponds to a CAPTCHA-enabled form must include the HTTP `X-Captcha` header, along with the text the shopper entered in response to the CAPTCHA challenge. + +Form name | Mutation +--- | --- +Add Gift Card Code | `applyGiftCardToCart` +Applying Coupon Code | `applyCouponToCart` +Change password | `changeCustomerPassword` +Checkout/Placing Order | `setPaymentMethodOnCart`, `setPaymentMethodAndPlaceOrder` +Contact Us | Not applicable +Create company | Not applicable +Create user | `createCustomer` +Forgot password | Not applicable +Login | `generateCustomerToken` +Payflow Pro | `setPaymentMethodOnCart`, `setPaymentMethodAndPlaceOrder` +Send to Friend Form | `sendEmailToFriend` +Share Wishlist Form | Not applicable + +## reCAPTCHA + +The following table lists the forms that can be configured to require reCAPTCHA. Go to **Stores** > **Configuration** > **Security** > **Google reCAPTCHA Storefront** > **Storefront** to enable or disable reCAPTCHA on these forms. If reCAPTCHA is enabled, unless an integration token is provided, always specify the HTTP `X-ReCaptcha` header and the value generated by the Google API. + +Field name | Mutation +--- | --- +Enable for Customer Login | `generateCustomerToken` +Enable for Forgot Password | `changeCustomerPassword` +Enable for Create New Customer Account | `createCustomer` +Enable for Edit Customer Account | `updateCustomer` +Enable for Contact Us | Not applicable +Enable for Product Review | `createProductReview` +Enable for Newsletter Subscription | `subscribeEmailToNewsletter` +Enable for Send To Friend | `sendEmailToFriend` +Enable for PayPal PayflowPro payment form | `createPayflowProToken` +Enable for Braintree payment form | Not applicable +Enable for Checkout/Placing Order | `setPaymentMethodOnCart`, `setPaymentMethodAndPlaceOrder` +Enable for Coupon Codes | `applyCouponToCart` + +{:.ref-header} +Related topics + +[Construct a request]({{page.baseurl}}/get-started/gs-web-api-request.html) diff --git a/src/guides/v2.3/graphql/queries/cart.md b/src/guides/v2.3/graphql/queries/cart.md new file mode 100644 index 00000000000..8e2d7a5e7d9 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/cart.md @@ -0,0 +1,800 @@ +--- +group: graphql +title: cart query +redirect_from: + - /guides/v2.3/graphql/reference/quote.html +--- + +Use the `cart` query to retrieve information about a particular cart. + +Cart functionality is defined in the `Quote` module. A Quote represents the contents of a customer's shopping cart. It is responsible for performing tasks such as: + +* Tracking each item in the cart, including the quantity and base cost +* Determining estimated shipping costs +* Calculating subtotals, computing additional costs, applying coupons, and determining the payment method + +## Syntax + +`{cart(cart_id: String!) {Cart}}` + +## Sample queries + +### Cart ready for checkout + +The following query shows the status of a cart that is ready to be converted into an order. + +**Request:** + +```graphql +{ + cart(cart_id: "CYmiiQRjPVc2gJUc5r7IsBmwegVIFO43") { + email + billing_address { + city + country { + code + label + } + firstname + lastname + postcode + region { + code + label + } + street + telephone + } + shipping_addresses { + firstname + lastname + street + city + region { + code + label + } + country { + code + label + } + telephone + available_shipping_methods { + amount { + currency + value + } + available + carrier_code + carrier_title + error_message + method_code + method_title + price_excl_tax { + value + currency + } + price_incl_tax { + value + currency + } + } + selected_shipping_method { + amount { + value + currency + } + carrier_code + carrier_title + method_code + method_title + } + } + items { + id + product { + name + sku + } + quantity + } + available_payment_methods { + code + title + } + selected_payment_method { + code + title + } + applied_coupons { + code + } + prices { + grand_total { + value + currency + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "cart": { + "email": "roni_cost@example.com", + "billing_address": { + "city": "Calder", + "country": { + "code": "US", + "label": "US" + }, + "firstname": "Veronica", + "lastname": "Costello", + "postcode": "49628-7978", + "region": { + "code": "MI", + "label": "Michigan" + }, + "street": [ + "6146 Honey Bluff Parkway" + ], + "telephone": "(555) 229-3326" + }, + "shipping_addresses": [ + { + "firstname": "Veronica", + "lastname": "Costello", + "street": [ + "6146 Honey Bluff Parkway" + ], + "city": "Calder", + "region": { + "code": "MI", + "label": "Michigan" + }, + "country": { + "code": "US", + "label": "US" + }, + "telephone": "(555) 229-3326", + "available_shipping_methods": [ + { + "amount": { + "currency": "USD", + "value": 10 + }, + "available": true, + "carrier_code": "tablerate", + "carrier_title": "Best Way", + "error_message": "", + "method_code": "bestway", + "method_title": "Table Rate", + "price_excl_tax": { + "value": 10, + "currency": "USD" + }, + "price_incl_tax": { + "value": 10, + "currency": "USD" + } + }, + { + "amount": { + "currency": "USD", + "value": 15 + }, + "available": true, + "carrier_code": "flatrate", + "carrier_title": "Flat Rate", + "error_message": "", + "method_code": "flatrate", + "method_title": "Fixed", + "price_excl_tax": { + "value": 15, + "currency": "USD" + }, + "price_incl_tax": { + "value": 15, + "currency": "USD" + } + } + ], + "selected_shipping_method": { + "amount": { + "value": 10, + "currency": "USD" + }, + "carrier_code": "tablerate", + "carrier_title": "Best Way", + "method_code": "bestway", + "method_title": "Table Rate" + } + } + ], + "items": [ + { + "id": "14", + "product": { + "name": "Strive Shoulder Pack", + "sku": "24-MB04" + }, + "quantity": 2 + }, + { + "id": "17", + "product": { + "name": "Savvy Shoulder Tote", + "sku": "24-WB05" + }, + "quantity": 1 + } + ], + "available_payment_methods": [ + { + "code": "braintree_cc_vault", + "title": "Stored Cards (Braintree)" + }, + { + "code": "braintree", + "title": "Credit Card (Braintree)" + }, + { + "code": "checkmo", + "title": "Check / Money order" + } + ], + "selected_payment_method": { + "code": "checkmo", + "title": "Check / Money order" + }, + "applied_coupons": null, + "prices": { + "grand_total": { + "value": 105.26, + "currency": "USD" + } + } + } + } +} +``` + +### Cart discounts + +In this query, the **Buy 3 tee shirts and get the 4th free** cart price rule from the sample data is active. This rule was modified slightly to add the label `3T1free`. (If a cart price rule does not have a label, Magento returns a default label of `Discount`.) A custom rule in which the customer saves 10% on the order by applying a discount code is also in effect. + +The `3T1free` rule is applied first, and Magento returns the price of a single shirt, $29, as the discount. Magento then applies a 10% discount to the remaining total of the products in the cart. + +**Request:** + +```graphql +{ + cart(cart_id: "v7jYJUjvPeHbdMJRcOfZIeQhs2Xc2ZKT") { + email + items { + id + prices { + total_item_discount { + value + } + price { + value + } + discounts { + label + amount { + value + } + } + } + product { + name + sku + } + quantity + } + applied_coupons { + code + } + prices { + discounts { + amount { + value + } + label + } + grand_total { + value + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "cart": { + "email": "roni_cost@example.com", + "items": [ + { + "id": "43", + "prices": { + "total_item_discount": { + "value": 37.7 + }, + "price": { + "value": 29 + }, + "discounts": [ + { + "label": "3T1free", + "amount": { + "value": 29 + } + }, + { + "label": "10% Off for New Customers", + "amount": { + "value": 8.7 + } + } + ] + }, + "product": { + "name": "Elisa EverCool™ Tee", + "sku": "WS06" + }, + "quantity": 4 + } + ], + "applied_coupons": [ + { + "code": "NEW" + } + ], + "prices": { + "discounts": [ + { + "amount": { + "value": 29 + }, + "label": "3T1free" + }, + { + "amount": { + "value": 8.7 + }, + "label": "10% Off for New Customers" + } + ], + "grand_total": { + "value": 84.76 + } + } + } + } +} +``` + +### Tier price example + +In the following example, tier prices has been established for product `24-UG01` and `24-UG05`, as shown in the following table: + +Product | Quantity | Fixed/Discount | Amount +--- | --- | --- | --- | +24-UG01 | 5 | Discount | 5% +24-UG01 | 10 | Discount | 10% +24-UG01 | 15 | Discount | 15% +24-UG05 | 5 | Fixed | $16 +24-UG05 | 10 | Fixed | $11 + +The cart in the example contains 12 units of `24-UG05` and 8 units of `24-UG-01`, so the price of `24-UG05` is $11, and the price of `24-UG01` is $18.05 (5% off). + +**Request:** + +```graphql +query { + cart(cart_id: "v7jYJUjvPeHbdMJRcOfZIeQhs2Xc2ZKT"){ + items { + id + quantity + product{ + name + sku + price_tiers { + quantity + final_price { + value + } + discount { + amount_off + percent_off + } + } + } + prices{ + price{ + value + } + } + } + prices { + discounts { + label + amount { + value + } + } + subtotal_excluding_tax { + value + } + applied_taxes { + label + amount { + value + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "cart": { + "items": [ + { + "id": "65", + "quantity": 12, + "product": { + "name": "Go-Get'r Pushup Grips", + "sku": "24-UG05", + "price_tiers": [ + { + "quantity": 5, + "final_price": { + "value": 16 + }, + "discount": { + "amount_off": 3, + "percent_off": 15.79 + } + }, + { + "quantity": 10, + "final_price": { + "value": 11 + }, + "discount": { + "amount_off": 8, + "percent_off": 42.11 + } + } + ] + }, + "prices": { + "price": { + "value": 11 + } + } + }, + { + "id": "66", + "quantity": 8, + "product": { + "name": "Quest Lumaflex™ Band", + "sku": "24-UG01", + "price_tiers": [ + { + "quantity": 5, + "final_price": { + "value": 18.05 + }, + "discount": { + "amount_off": 0.95, + "percent_off": 5 + } + }, + { + "quantity": 10, + "final_price": { + "value": 17.1 + }, + "discount": { + "amount_off": 1.9, + "percent_off": 10 + } + }, + { + "quantity": 15, + "final_price": { + "value": 16.15 + }, + "discount": { + "amount_off": 2.85, + "percent_off": 15 + } + } + ] + }, + "prices": { + "price": { + "value": 18.05 + } + } + } + ], + "prices": { + "discounts": [ + { + "label": "200", + "amount": { + "value": 55.28 + } + } + ], + "subtotal_excluding_tax": { + "value": 276.4 + }, + "applied_taxes": [ + { + "label": "US-MI-*-Rate 1", + "amount": { + "value": 18.24 + } + } + ] + } + } + } +} +``` + +## Input attributes + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | A 32-character string that is created when you [create a cart]({{page.baseurl}}/graphql/mutations/create-empty-cart.html) + +## Output attributes {#cart-output} + +The top-level `Cart` object is listed first. All interfaces and child objects are listed in alphabetical order. + +### Cart object + +The `Cart` object can contain the following attributes. + +{% include graphql/cart-object.md %} + +### AppliedCoupon object {#AppliedCoupon} + +The `AppliedCoupon` object must contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`code` | String! | The coupon code applied to the order + +### AppliedGiftCard object {#AppliedGiftCard} + +The `AppliedGiftCard` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`applied_balance` | Money | Applied balance to the current cart +`code` | String | The gift card code applied to the order +`current_balance` | Money | Current balance remaining on the gift card +`expiration_date` | String | Gift card expiration date + +### AppliedStoreCredit object {#AppliedStoreCredit} + +The `AppliedStoreCredit` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`applied_balance` | Money | The amount of store credit applied to the current cart +`current_balance` | Money | The customer's store credit balance before applying store credit to the cart +`enabled` | Boolean | Indicates whether store credits are enabled. If the feature is disabled, then the current balance will not be returned + +### AvailablePaymentMethod object {#AvailablePaymentMethod} + +The `AvailablePaymentMethod` object must contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`code` | String! | The payment method code +`title` | String! | The payment method title + +### AvailableShippingMethod object {#AvailableShippingMethod} + +The `AvailableShippingMethod` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`amount` | Money! | The cost of shipping using this shipping method +`available` | Boolean! | Indicates whether this shipping method can be applied to the cart +`base_amount` | Money | Deprecated. This attribute is not applicable for GraphQL +`carrier_code` | String! | A string that identifies a commercial carrier or an offline shipping method +`carrier_title` | String! | The label for the carrier code +`error_message` | String | Describes an error condition +`method_code` | String | A shipping method code associated with a carrier. Could be null if method is not available +`method_title` | String | The label for the method code. Could be null if method is not available +`price_excl_tax` | Money! | The cost of shipping using this shipping method, excluding tax +`price_incl_tax` | Money! | The cost of shipping using this shipping method, excluding tax + +### BillingCartAddress object {#BillingCartAddress} + +The `BillingCartAddress` object implements [`CartAddressInterface`](#CartAddressInterface). It does not define any additional attributes. + +### CartAddressCountry object {#CartAddressCountry} + +The `CartAddressCountry` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`code` | String! | The country code +`label` | String! | The display label for the country + +### CartAddressInterface {#CartAddressInterface} + +The `CartAddressInterface` contains the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`city` | String! | The city specified for the billing address +`company` | String | The company specified for the billing address +`country` | [CartAddressCountry!](#CartAddressCountry) | The country code and label for the billing address +`customer_notes` | String | Comments made to the customer that accompanies the order +`firstname` | String! | The customer's first name +`lastname` | String! | The customer's last name +`postcode` | String | The postal code for the billing address +`region` | [CartAddressRegion](#CartAddressRegion) | An object containing the region label and code +`street` | [String!]! | The street for the billing address +`telephone` | String! | The telephone number for the billing address + +### CartAddressRegion object {#CartAddressRegion} + +The `CartAddressRegion` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`code` | String! | The state or province code +`label` | String! | The display label for the region + +### CartDiscount object {#CartDiscount} + +The `CartDiscount` object must contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`amount` | Money | The amount of all discounts applied to the cart +`label` | [String!]! | A concatenated list of strings that describe each applied discount + +### CartItemInterface {#CartItemInterface} + +The `CartItemInterface` can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`id` | String | ID of the item +`prices` | [CartItemPrices](#CartItemPrices) | Includes the price of an item, any applied discounts, and calculated totals +`product` | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html) | Contains attributes that are common to all types of products +`quantity` | Float | The number of items in the cart + +### CartItemPrices object {#CartItemPrices} + +The `CartItemPrices` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`discounts`| [[Discount]](#Discount) | An array of discounts to be applied to the cart item +`price` | Money! | The price of the item before any discounts were applied +`row_total` | Money! | The value of the `price` multiplied by the quantity of the item +`row_total_including_tax` | Money! | The value of `row_total` plus the tax applied to the item +`total_item_discount` | Money | The total of all discounts applied to the item + +### CartItemQuantity object {#CartItemQuantity} + +The `CartItemQuantity` data type has been deprecated. Use the `cart_items_v2` attribute with the [`CartItemInterface`](#CartItemInterface) instead. + +Attribute | Data Type | Description +--- | --- | --- +`cart_item_id` | Int! | Deprecated. Use `CartItemInterface.id` instead +`quantity` | Float! | Deprecated. Use `CartItemInterface.quantity` instead + +### CartPrices object {#CartPrices} + +The `CartPrices` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`applied_taxes` | [[CartTaxItem]](#CartTaxItem) | An array containing the names and amounts of taxes applied to the item +`discount` | [CartDiscount](#CartDiscount) | Deprecated. Use `discounts` instead +`discounts` | [[Discount]](#Discount) | An array containing all discounts applied to the cart +`grand_total` | Money | The total, including discounts, taxes, shipping, and other fees +`subtotal_excluding_tax` | Money | Subtotal without taxes +`subtotal_including_tax` | Money | Subtotal with taxes +`subtotal_with_discount_excluding_tax` | Money | Subtotal with any discounts applied, but not taxes + +### CartTaxItem object {#CartTaxItem} + +The `CartTaxItem` object must contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`amount` | Money! | The amount of tax applied to the item +`label` | String! | The description of the tax + +### Discount object {#Discount} + +A discount can be applied to the cart as a whole or to an item. + +If a cart rule does not have a label, Magento uses `Discount` as the default label. + +The `Discount` object must contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`amount` | Money! | The amount of the discount applied to the cart +`label` | String! | The description of the discount + +### SelectedPaymentMethod object {#SelectedPaymentMethod} + +The `SelectedPaymentMethod` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`code` | String! | The payment method code +`purchase_order_number` | String | The purchase order number +`title` | String! | The payment method title + +### SelectedShippingMethod object {#SelectedShippingMethod} + +The `SelectedShippingMethod` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`amount` | Money! | The cost of shipping using this shipping method +`base_amount` | Money | Deprecated. This attribute is not applicable for GraphQL +`carrier_code` | String! | A string that identifies a commercial carrier or an offline shipping method +`carrier_title` | String! | The label for the carrier code +`method_code` | String! | A shipping method code associated with a carrier +`method_title` | String! | The label for the method code + +### ShippingCartAddress object {#ShippingCartAddress} + +The `ShippingCartAddress` object implements [`CartAddressInterface`](#CartAddressInterface). It can also contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`available_shipping_methods` | [[AvailableShippingMethod]](#AvailableShippingMethod) | An array that lists the shipping methods that can be applied to the cart +`cart_items` | [[CartItemQuantity]](#CartItemQuantity) | Deprecated. Use `cart_items_v2` instead +`cart_items_v2` | [CartItemInterface] | An array that lists the items in the cart +`items_weight` | Float | Deprecated. This attribute is not applicable for GraphQL +`selected_shipping_method` | [SelectedShippingMethod](#SelectedShippingMethod) | An object that describes the selected shipping method + +## Related topics + +* [createEmptyCart mutation]({{page.baseurl}}/graphql/mutations/create-empty-cart.html) +* [addSimpleProductsToCart mutation]({{page.baseurl}}/graphql/mutations/add-simple-products.html) +* [setShippingAddressesOnCart mutation]({{page.baseurl}}/graphql/mutations/set-shipping-address.html) +* [setShippingMethodsOnCart mutation]({{page.baseurl}}/graphql/mutations/set-shipping-method.html) +* [setBillingAddressOnCart mutation]({{page.baseurl}}/graphql/mutations/set-billing-address.html) +* [setPaymentMethodOnCart mutation]({{page.baseurl}}/graphql/mutations/set-payment-method.html) + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID \"xxxxx\"` | The ID provided in the `cart_id` field is invalid or the cart does not exist for the customer. +`The cart isn't active` | The cart with the specified cart ID is unavailable, because the items have been purchased and the cart ID becomes inactive. +`Field cart.cart_id of required type String! was not provided` | The value specified in the `cart.cart_id` argument is empty. \ No newline at end of file diff --git a/src/guides/v2.3/graphql/queries/category-list.md b/src/guides/v2.3/graphql/queries/category-list.md new file mode 100644 index 00000000000..98ca81871bd --- /dev/null +++ b/src/guides/v2.3/graphql/queries/category-list.md @@ -0,0 +1,401 @@ +--- +group: graphql +title: categoryList query +--- + +The `categoryList` query searches for categories that match the criteria specified in filters. It replaces the deprecated `category` query, which allowed you to search by category ID only. + +The `categoryList` query supports the following types of filters. You can specify multiple filters in a query. + +- Category ID +- Category name +- URL Key + +If you do not provide any filter input, the query returns the root category. + +The query returns a `CategoryTree` object. The top level of the `CategoryTree` object provides details about the queried category. This object includes the `children` attribute, which contains an array of its immediate subcategories. To return multiple category levels in a single call, define the response so that it contains up to ten nested `children` options. + +{:.bs-callout-info} +You cannot return the entire category tree if it contains more than ten sublevels unless the `queryDepth` parameter in the GraphQL `di.xml` file has been reconfigured. + +Use the `breadcrumbs` attribute to return information about the parent categories of the queried category. + +## Syntax + +```graphql +categoryList ( + filters: CategoryFilterInput +): CategoryTree +``` + +## Example usage + +### Return the category tree of a top-level category + +The following query returns information about category IDs `11` and `20` and two levels of subcategories. In the sample data, category IDs `11` and `20` are assigned to the `Men` and `Women` categories, respectively. + +**Request:** + +```graphql +{ + categoryList(filters: {ids: {in: ["11", "20"]}}) { + children_count + children { + id + level + name + path + url_path + url_key + children { + id + level + name + path + url_path + url_key + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "categoryList": [ + { + "children_count": "8", + "children": [ + { + "id": 13, + "level": 3, + "name": "Bottoms", + "path": "1/2/11/13", + "url_path": "men/bottoms-men", + "url_key": "bottoms-men", + "children": [ + { + "id": 18, + "level": 4, + "name": "Pants", + "path": "1/2/11/13/18", + "url_path": "men/bottoms-men/pants-men", + "url_key": "pants-men" + }, + { + "id": 19, + "level": 4, + "name": "Shorts", + "path": "1/2/11/13/19", + "url_path": "men/bottoms-men/shorts-men", + "url_key": "shorts-men" + } + ] + }, + { + "id": 12, + "level": 3, + "name": "Tops", + "path": "1/2/11/12", + "url_path": "men/tops-men", + "url_key": "tops-men", + "children": [ + { + "id": 14, + "level": 4, + "name": "Jackets", + "path": "1/2/11/12/14", + "url_path": "men/tops-men/jackets-men", + "url_key": "jackets-men" + }, + { + "id": 15, + "level": 4, + "name": "Hoodies & Sweatshirts", + "path": "1/2/11/12/15", + "url_path": "men/tops-men/hoodies-and-sweatshirts-men", + "url_key": "hoodies-and-sweatshirts-men" + }, + { + "id": 16, + "level": 4, + "name": "Tees", + "path": "1/2/11/12/16", + "url_path": "men/tops-men/tees-men", + "url_key": "tees-men" + }, + { + "id": 17, + "level": 4, + "name": "Tanks", + "path": "1/2/11/12/17", + "url_path": "men/tops-men/tanks-men", + "url_key": "tanks-men" + } + ] + } + ] + }, + { + "children_count": "8", + "children": [ + { + "id": 22, + "level": 3, + "name": "Bottoms", + "path": "1/2/20/22", + "url_path": "women/bottoms-women", + "url_key": "bottoms-women", + "children": [ + { + "id": 27, + "level": 4, + "name": "Pants", + "path": "1/2/20/22/27", + "url_path": "women/bottoms-women/pants-women", + "url_key": "pants-women" + }, + { + "id": 28, + "level": 4, + "name": "Shorts", + "path": "1/2/20/22/28", + "url_path": "women/bottoms-women/shorts-women", + "url_key": "shorts-women" + } + ] + }, + { + "id": 21, + "level": 3, + "name": "Tops", + "path": "1/2/20/21", + "url_path": "women/tops-women", + "url_key": "tops-women", + "children": [ + { + "id": 23, + "level": 4, + "name": "Jackets", + "path": "1/2/20/21/23", + "url_path": "women/tops-women/jackets-women", + "url_key": "jackets-women" + }, + { + "id": 24, + "level": 4, + "name": "Hoodies & Sweatshirts", + "path": "1/2/20/21/24", + "url_path": "women/tops-women/hoodies-and-sweatshirts-women", + "url_key": "hoodies-and-sweatshirts-women" + }, + { + "id": 25, + "level": 4, + "name": "Tees", + "path": "1/2/20/21/25", + "url_path": "women/tops-women/tees-women", + "url_key": "tees-women" + }, + { + "id": 26, + "level": 4, + "name": "Bras & Tanks", + "path": "1/2/20/21/26", + "url_path": "women/tops-women/tanks-women", + "url_key": "tanks-women" + } + ] + } + ] + } + ] + } +} +``` + +### Return breadcrumb information + +The following query returns breadcrumb information about categories that have the name `Tops`. + +**Request:** + +```graphql +{ + categoryList(filters: {name: {match: "Tops"}}) { + id + level + name + breadcrumbs { + category_id + category_name + category_level + category_url_key + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "categoryList": [ + { + "id": 12, + "level": 3, + "name": "Tops", + "breadcrumbs": [ + { + "category_id": 11, + "category_name": "Men", + "category_level": 2, + "category_url_key": "men" + } + ] + }, + { + "id": 21, + "level": 3, + "name": "Tops", + "breadcrumbs": [ + { + "category_id": 20, + "category_name": "Women", + "category_level": 2, + "category_url_key": "women" + } + ] + } + ] + } +} +``` + +### Return the categoryList by url_key filters + +The following query returns information about the Gear category using a `url_key` filter. You must pass the `url_key` value without a suffix and specify either the `eq` or `in` keyword. + +**Request:** + +```graphql +{ + categoryList(filters: { url_key: { eq: "gear" } }) { + id + level + name + path + url_path + url_key + children_count + } +} +``` + +**Response:** + +```json +{ + "data": { + "categoryList": [ + { + "id": 3, + "level": 2, + "name": "Gear", + "path": "1/2/3", + "url_path": "gear", + "url_key": "gear", + "children_count": "3" + } + ] + } +} +``` + +### Return the categoryList by url_path filters + +The following query returns information about the Gear > Bags category using the `url_path` filter. Do not specify a suffix in the `url_path` value. The `url_path` filter accepts either the `eq` or `in` keyword. + +**Request:** + +```graphql +{ + categoryList(filters: { url_path: { eq: "gear/bags" } }) { + id + level + name + path + url_path + url_key + children_count + } +} +``` + +**Response:** + +```json +{ + "data": { + "categoryList": [ + { + "id": 4, + "level": 3, + "name": "Bags", + "path": "1/2/3/4", + "url_path": "gear/bags", + "url_key": "bags", + "children_count": "0" + } + ] + } +} +``` + +## Input attributes + +You must specify the `filters` attribute as input to your query. + +Attribute | Data type | Description +--- | --- | --- +`filters` | CategoryFilterInput | Contains filter definitions + +### CategoryFilterInput object + +The `CategoryFilterInput` object defines the filters to be used in this query. + +Attribute | Data type | Description +--- | --- | --- +`ids` | FilterEqualTypeInput | Filters by the specified category IDs +`name` | FilterMatchTypeInput | Filters by the display name of the category +`url_key` | FilterEqualTypeInput | Filters by the part of the URL that identifies the category +`url_path` | FilterEqualTypeInput | Filters by the URL path for the category + +### FilterEqualTypeInput object + +Use the `FilterEqualTypeInput` object to construct filters that search by category ID or URL key. + +Attribute | Data type | Description +--- | --- | --- +`eq` | String | Use this attribute to exactly match the specified string. For example, to filter on a specific URL key, specify a value like `shorts-women` +`in` | [String] | Use this attribute to filter on an array of values. For example, to filter on category IDs 4, 5, and 6, specify a value of `["4", "5", "6"]` + +### FilterMatchTypeInput object + +Use the `FilterMatchTypeInput` object to construct a filter that matches the specified display name. + +Attribute | Data type | Description +--- | --- | --- +`match` | String | Use this attribute to perform a fuzzy match on the specified string. For example, to filter on a specific category name, specify a value such as `Tops` + +## Output attributes {#Categories} + +The query returns a `CategoryTree` object, which implements [`CategoryInterface`]({{page.baseurl}}/graphql/interfaces/category-interface.html). The `CategoryTree` object can contain the following attribute and all attributes defined in `CategoryInterface`: + +Attribute | Data type | Description +--- | --- | --- +`children` | `CategoryTree` | An array containing the next level of subcategories. By default, you can specify up to 10 levels of child categories diff --git a/src/guides/v2.3/graphql/queries/category.md b/src/guides/v2.3/graphql/queries/category.md new file mode 100644 index 00000000000..d383d685f0f --- /dev/null +++ b/src/guides/v2.3/graphql/queries/category.md @@ -0,0 +1,221 @@ +--- +group: graphql +title: category query +redirect_from: + - /guides/v2.3/graphql/reference/categories.html +--- + +{:.bs-callout-warning} +The `category` query has been deprecated. Use the [categoryList]({{page.baseurl}}/graphql/queries/category-list.html) query instead. + +The `category` query allows you to search for a single category definition or the entire category tree. To return multiple category levels in a single call, define the response so that it contains up to ten nested `children` options. You cannot return the entire category tree if it contains more than 10 sublevels unless the `queryDepth` parameter in the GraphQL `di.xml` file has been reconfigured. + +## Syntax + +```graphql +category ( + id: int +): CategoryTree +``` + +## Example usage + +### Return the category tree of a top-level category + +The following query returns information about category ID `20` and four levels of subcategories. In the sample data, category ID `20` is assigned to the `Women` category. + +**Request:** + +```graphql +{ + category(id: 20) { + products { + total_count + page_info { + current_page + page_size + } + } + children_count + children { + id + level + name + path + children { + id + level + name + path + children { + id + level + name + path + children { + id + level + name + path + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "category": { + "products": { + "total_count": 0, + "page_info": { + "current_page": 1, + "page_size": 20 + } + }, + "children_count": "8", + "children": [ + { + "id": 22, + "level": 3, + "name": "Bottoms", + "path": "1/2/20/22", + "children": [ + { + "id": 27, + "level": 4, + "name": "Pants", + "path": "1/2/20/22/27", + "children": [] + }, + { + "id": 28, + "level": 4, + "name": "Shorts", + "path": "1/2/20/22/28", + "children": [] + } + ] + }, + { + "id": 21, + "level": 3, + "name": "Tops", + "path": "1/2/20/21", + "children": [ + { + "id": 23, + "level": 4, + "name": "Jackets", + "path": "1/2/20/21/23", + "children": [] + }, + { + "id": 24, + "level": 4, + "name": "Hoodies & Sweatshirts", + "path": "1/2/20/21/24", + "children": [] + }, + { + "id": 25, + "level": 4, + "name": "Tees", + "path": "1/2/20/21/25", + "children": [] + }, + { + "id": 26, + "level": 4, + "name": "Bras & Tanks", + "path": "1/2/20/21/26", + "children": [] + } + ] + } + ] + } + } +} +``` + +### Return breadcrumb information + +The following query returns breadcrumb information about the women's `Tops` category (`id` = 25). + +**Request:** + +```graphql +{ + category ( + id: 25 +) { + id + level + name + breadcrumbs { + category_id + category_name + category_level + category_url_key + category_url_path + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "category": { + "id": 25, + "level": 4, + "name": "Tees", + "breadcrumbs": [ + { + "category_id": 20, + "category_name": "Women", + "category_level": 2, + "category_url_key": "women", + "category_url_path": "women" + }, + { + "category_id": 21, + "category_name": "Tops", + "category_level": 3, + "category_url_key": "tops-women", + "category_url_path": "women/tops-women" + } + ] + } + } +} +``` + +## Input attributes + +Attribute | Data type | Description +--- | --- | --- +`id` | Int | The category ID to use as the starting point of your category search + +## Output attributes {#Categories} + +The query returns a `CategoryTree` object, which implements [`CategoryInterface`]({{page.baseurl}}/graphql/interfaces/category-interface.html). The `CategoryTree` object can contain the following attribute, as we as all attributes defined in `CategoryInterface`: + +Attribute | Data type | Description +--- | --- | --- +`children` | `CategoryTree` | An array containing the next level of subcategories. By default, you can specify up to 10 levels of child categories + +## Errors + +Error | Description +--- | --- +`Category doesn't exist` | The specified category ID value does not exist. +`Field "category" argument "id" requires type Int, found "XXX"` | The specified `id` argument value has the wrong type. \ No newline at end of file diff --git a/src/guides/v2.3/graphql/queries/checkout-agreements.md b/src/guides/v2.3/graphql/queries/checkout-agreements.md new file mode 100644 index 00000000000..610c054f548 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/checkout-agreements.md @@ -0,0 +1,71 @@ +--- +group: graphql +title: checkoutAgreements query +contributor_name: Something Digital +contributor_link: https://www.somethingdigital.com/ +--- + +The `checkoutAgreements` query retrieves checkout agreements. The query will always return an empty array when the +**Enable Terms and Conditions** option is set to **No**. (The config path is `checkout/options/enable_agreements`.) + +## Syntax + +`{checkoutAgreements {CheckoutAgreement}}` + +## Example usage + +The following query returns enabled checkout agreements. + +**Request:** + +```graphql +{ + checkoutAgreements { + agreement_id + checkbox_text + content + content_height + is_html + mode + name + } +} +``` + +**Response:** + +```json +{ + "data": { + "checkoutAgreements": [ + { + "agreement_id": 1, + "checkbox_text": "I agree to the terms of sale", + "content": "

    Agreement Contents

    \r\n

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    ", + "content_height": "22px", + "is_html": true, + "mode": "AUTO", + "name": "My Agreement" + } + ] + } +} +``` + +## Output attributes + +The `CheckoutAgreements` object contains an array of [`CheckoutAgreement`](#checkoutAgreementAttributes) objects. + +### CheckoutAgreement attributes {#checkoutAgreementAttributes} + +The `CheckoutAgreement` object provides the following attributes: + +Attribute | Data type | Description +--- | --- | --- +`agreement_id` | Integer! | Checkout Agreement identifier +`checkbox_text` | String! | Label of the Checkout Agreement checkbox +`content` | String! | The content of the Checkout Agreement. The value can be in plain text or in HTML +`content_height` | String | CSS height of Checkout Agreement +`is_html` | Boolean! | Is Checkout Agreement content in HTML format +`mode` | String! | Indicates whether terms and conditions are applied manually (`MANUAL`) or automatically (`AUTO`) +`name` | String! | Checkout Agreement name diff --git a/src/guides/v2.3/graphql/queries/cms-blocks.md b/src/guides/v2.3/graphql/queries/cms-blocks.md new file mode 100644 index 00000000000..2c61918545e --- /dev/null +++ b/src/guides/v2.3/graphql/queries/cms-blocks.md @@ -0,0 +1,70 @@ +--- +group: graphql +title: cmsBlocks query +--- + +The `cmsBlocks` query returns information about blocks that were developed with the Magento Content Management System (CMS). + +## Syntax + +Return the contents of one or more CMS blocks: + +`cmsBlocks(identifiers: [String]): CmsBlocks` + +## Example usage + +The following query returns information about the `login-data` block: + +**Request:** + +```graphql +{ + cmsBlocks(identifiers: "login-data") { + items { + identifier + title + content + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "cmsBlocks": { + "items": [ + { + "identifier": "login-data", + "title": "Login Info Block", + "content": "
    \n

    Try Demo Customer Access

    \n

    Email:roni_cost@example.com

    \n

    Password:roni_cost3@example.com

    \n
    " + } + ] + } + } +} +``` + +## Input attributes + +Attribute | Data type | Description +--- | --- | --- +`id` | Int | Deprecated. Use `identifier` instead. +`identifiers` | [String] | An array containing a comma-separated list of block identifiers + +## Output attributes + +The `CmsBlocks` object contains an array of `items`, each of which can contain a `CmsBlock` object. + +### CmsBlock attributes + +{% include graphql/cms-block-object.md %} + +## Errors + +Error | Description +--- | --- +`The CMS block with the "XXXX" ID doesn't exist` | The specified CMS block ID is invalid. +`"identifiers" of CMS blocks should be specified"` | The `identifiers` array parameter is required for identifying the CMS blocks. diff --git a/src/guides/v2.3/graphql/queries/cms-page.md b/src/guides/v2.3/graphql/queries/cms-page.md new file mode 100644 index 00000000000..cb8a28d9175 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/cms-page.md @@ -0,0 +1,90 @@ +--- +group: graphql +title: cmsPage query +redirect_from: + - /guides/v2.3/graphql/reference/cms.html +--- + +The `cmsPage` query returns information about content pages that were developed with the Magento Content Management System (CMS). + +## Syntax + +Return the contents of a CMS page: + +`cmsPage(identifier: String): CmsPage` + +## Example usage + +You must include the CMS page identifier value to retrieve the content of a specific CMS page. The following query returns information about the "404 Not Found" CMS page: + +**Request:** + +```graphql +{ + cmsPage(identifier: "no-route") { + identifier + url_key + title + content + content_heading + page_layout + meta_title + meta_description + meta_keywords + } +} +``` + +**Response:** + +```json +{ + "data": { + "cmsPage": { + "identifier": "no-route" + "url_key": "no-route", + "title": "404 Not Found", + "content": "
    \r\n
    The page you requested was not found, and we have a fine guess why.
    \r\n
    \r\n
      \r\n
    • If you typed the URL directly, please make sure the spelling is correct.
    • \r\n
    • If you clicked on a link to get here, the link is outdated.
    • \r\n
    \r\n
    \r\n
    \r\n
    What can you do?
    \r\n
    Have no fear, help is near! There are many ways you can get back on track with Magento Store.
    \r\n
    \r\n
      \r\n
    • Go back to the previous page.
    • \r\n
    • Use the search bar at the top of the page to search for your products.
    • \r\n
    • Follow these links to get you back on track!
      Store Home | My Account
    \r\n", + "content_heading": "Whoops, our bad...", + "page_layout": "2columns-right", + "meta_title": null, + "meta_description": "Page description", + "meta_keywords": "Page keywords" + } + } +} +``` + +## Input attributes + +Attribute | Data type | Description +--- | --- | --- +`id` | Int | Deprecated. Use `identifier` instead. +`identifier` | String | The identifier of a CMS page + +## Output attributes + +The `CmsPage` object can contain the following attributes: + +Attribute | Data type | Description +--- | --- | --- +`content` | String | The content of the CMS page in raw HTML +`content_heading` | String | The heading that displays at the top of the CMS page +`identifier` | String | The identifier of the CMS page +`meta_description` | String | A brief description of the page for search results listings +`meta_keywords` | String | A set of keywords that search engines can use to index the page +`meta_title` | String | A page title that is indexed by search engines and appears in search results listings +`page_layout` | String | The design layout of the page, indicating the number of columns and navigation features used on the page +`title` | String | The name that appears in the breadcrumb trail navigation and in the browser title bar and tab +`url_key` |String | The URL key of the CMS page, which is often based on the `content_heading` + +## Related topics + +[cmsBlocks query]({{page.baseurl}}/graphql/queries/cms-blocks.html) + +## Errors + +Error | Description +--- | --- +`The CMS page with the "XXXX" ID doesn't exist` | The specified CMS page ID is invalid. +`Page id/identifier should be specified"` | The `identifier` parameter is required for identifying the CMS page. \ No newline at end of file diff --git a/src/guides/v2.3/graphql/queries/custom-attribute-metadata.md b/src/guides/v2.3/graphql/queries/custom-attribute-metadata.md new file mode 100644 index 00000000000..101d94c6027 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/custom-attribute-metadata.md @@ -0,0 +1,245 @@ +--- +group: graphql +title: customAttributeMetadata query +redirect_from: + - /guides/v2.3/graphql/reference/custom-attribute-metadata.html +--- + +The `customAttributeMetadata` query returns the attribute type, given an attribute code and entity type. All entity attributes can be added to an equivalent GraphQL type, including custom, extension, and EAV (which have precedence set in that order for collisions). The GraphQL query consumer does not have the ability to know a field's attribute type. + +## Syntax + +`customAttributeMetadata(attributes: [AttributeInput!]!): CustomAttributeMetadata` + +## Example usage + +The following query returns the attribute type for various custom and EAV attributes. + +**Request:** + +```graphql +{ + customAttributeMetadata( + attributes: [ + { + attribute_code: "size" + entity_type: "catalog_product" + } + { + attribute_code: "color" + entity_type: "catalog_product" + } + ] + ) { + items { + attribute_code + attribute_type + entity_type + input_type + attribute_options { + value + label + } + } + } +} + +``` + +**Response:** + +```json +{ + "data": { + "customAttributeMetadata": { + "items": [ + { + "attribute_code": "size", + "attribute_type": "Int", + "entity_type": "catalog_product", + "input_type": "select", + "attribute_options": [ + { + "value": "91", + "label": "55 cm" + }, + { + "value": "169", + "label": "XS" + }, + { + "value": "92", + "label": "65 cm" + }, + { + "value": "170", + "label": "S" + }, + { + "value": "93", + "label": "75 cm" + }, + { + "value": "171", + "label": "M" + }, + { + "value": "94", + "label": "6 foot" + }, + { + "value": "172", + "label": "L" + }, + { + "value": "95", + "label": "8 foot" + }, + { + "value": "173", + "label": "XL" + }, + { + "value": "96", + "label": "10 foot" + }, + { + "value": "174", + "label": "28" + }, + { + "value": "175", + "label": "29" + }, + { + "value": "176", + "label": "30" + }, + { + "value": "177", + "label": "31" + }, + { + "value": "178", + "label": "32" + }, + { + "value": "179", + "label": "33" + }, + { + "value": "180", + "label": "34" + }, + { + "value": "181", + "label": "36" + }, + { + "value": "182", + "label": "38" + } + ] + }, + { + "attribute_code": "color", + "attribute_type": "Int", + "entity_type": "catalog_product", + "input_type": "select", + "attribute_options": [ + { + "value": "49", + "label": "Black" + }, + { + "value": "50", + "label": "Blue" + }, + { + "value": "51", + "label": "Brown" + }, + { + "value": "52", + "label": "Gray" + }, + { + "value": "53", + "label": "Green" + }, + { + "value": "54", + "label": "Lavender" + }, + { + "value": "55", + "label": "Multi" + }, + { + "value": "56", + "label": "Orange" + }, + { + "value": "57", + "label": "Purple" + }, + { + "value": "58", + "label": "Red" + }, + { + "value": "59", + "label": "White" + }, + { + "value": "60", + "label": "Yellow" + } + ] + } + ] + } + } +} +``` + +## Input attributes + +The `AttributeInput` input object requires the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`attribute_code` | String | The unique identifier for an attribute code. This value should be in lowercase letters without spaces +`entity_type` | String | The type of entity that defines the attribute, such as `catalog_product`, `catalog_category`, or `customer` + +## Output attributes + +The `CustomAttributeMetadata` object is an array of `items`. The `items` object can contain the following attributes. + +Attribute | Data Type | Description +--- | --- | --- +`attribute_code` | String | The unique identifier for an attribute code. This value should be in lowercase letters without spaces +`attribute_options` | [`AttributeOption`] | An array of attribute options +`attribute_type` | String | The data type of the attribute +`entity_type` | String | The type of entity that defines the attribute, such as `catalog_product`, `catalog_category`, or `customer` +`input_type` | String | The frontend input type of the attribute + +### AttributeOption object + +The `AttributeOption` object contains the name and value of the option. + +Attribute | Data Type | Description +--- | --- | --- +`label` | String | The name of an attribute option +`value` | String | The value assigned to an attribute option + +## Errors + +Error | Description +--- | --- +`Field "customAttributeMetadata" argument "attributes" of type "[AttributeInput!]!" is required but not provided` | The `attributes` array parameter is required. +`The attribute with a "xxxx" attributeCode doesn't exist. Verify the attribute and try again` | The given `attribute_code` parameter is invalid. +`Invalid entity_type specified: "xxxx"` | The given `entity_type` is invalid. +`Missing attribute_code for the input entity_type: "xxxx"`| There is no value passed for the `attribute_code` parameter for the given `entity_type` parameter. +`Missing entity_type for the input attribute_code: "xxxx"` | There is no value passed for the `entity_type` parameter for the given `attribute_code` parameter. +`Missing attribute_code/entity_type for the input Empty AttributeInput` | There are no values passed for both `attribute_code` and `entity_type` parameters. \ No newline at end of file diff --git a/src/guides/v2.3/graphql/queries/customer-cart.md b/src/guides/v2.3/graphql/queries/customer-cart.md new file mode 100644 index 00000000000..3358396f727 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/customer-cart.md @@ -0,0 +1,82 @@ +--- +group: graphql +title: customerCart query +--- + +The `customerCart` query returns the active cart for the logged-in customer. If the cart does not exist, the query creates one. The customer's authorization token must be specified in the headers. + +The `customerCart` query differs from the `cart` query in the following ways: + +- The `customerCart` query must be run on behalf of a logged-in customer. If you run this query on behalf of a guest, an exception will be thrown. +- The `cart` query requires a `cart_id` value as input. The `customerCart` query does not take any input parameters. + +You can define the query to return the `id` attribute. You can use the value of this attribute as the `destination_cart_id` input parameter in the [`mergeCarts` mutation]({{page.baseurl}}/graphql/mutations/merge-carts.html). (The `mergeCarts` mutation provides the ability to merge a guest cart with the logged-in customer's cart.) + +{: .bs-callout-tip } +If you know the value of the logged-in customer's cart ID, you can allow the customer to start an order on one device and complete it on another. + +## Syntax + +`customerCart: Cart!` + +## Example usage + +The following query lists the products in the logged-in customer's cart: + +**Request:** + +```graphql +{ + customerCart { + id + items { + id + product { + name + sku + } + quantity + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "customerCart": { + "id": "CYmiiQRjPVc2gJUc5r7IsBmwegVIFO43", + "items": [ + { + "id": "11", + "product": { + "name": "Strive Shoulder Pack", + "sku": "24-MB04" + }, + "quantity": 1 + }, + { + "id": "12", + "product": { + "name": "Radiant Tee", + "sku": "WS12" + }, + "quantity": 1 + } + ] + } + } +} +``` + +## Output attributes + +The `customerCart` query returns the `Cart` object. + +### Cart object {#CartObject} + +{% include graphql/cart-object.md %} + +[Cart query output]({{page.baseurl}}/graphql/queries/cart.html#cart-output) provides more information about the `Cart` object. diff --git a/src/guides/v2.3/graphql/queries/customer-downloadable-products.md b/src/guides/v2.3/graphql/queries/customer-downloadable-products.md new file mode 100644 index 00000000000..95bce24b186 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/customer-downloadable-products.md @@ -0,0 +1,83 @@ +--- +group: graphql +title: customerDownloadableProducts query +--- + +Use the `customerDownloadableProducts` query to retrieve the list of purchased downloadable products for the logged-in customer. + +## Syntax + +`{customerDownloadableProducts: {CustomerDownloadableProducts}}` + +## Example usage + +The following example returns the list of purchased downloadable products for the logged-in customer. + +**Request:** + +```graphql +{ + customerDownloadableProducts { + items { + date + download_url + order_increment_id + remaining_downloads + status + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "customerDownloadableProducts": { + "items": [ + { + "date": "2019-03-04 20:48:32", + "download_url": "https:///downloadable/download/link/id/MC44NTcwMTEwMCAxNTUxNzMyNTEyMTExNTE%2C/", + "order_increment_id": "000000004", + "remaining_downloads": "Unlimited", + "status": "pending" + }, + { + "date": "2019-03-04 20:48:32", + "download_url": "https:///downloadable/download/link/id/MC44NzM0OTkwMCAxNTUxNzMyNTEyMjEyNTA%2C/", + "order_increment_id": "000000004", + "remaining_downloads": "Unlimited", + "status": "pending" + } + ] + } + } +} +``` + +## Output attributes + +The `CustomerDownloadableProducts` object contains the following attribute. + +Attribute | Type | Description +--- | --- | --- +`items` | [[CustomerDownloadableProduct]](#custDownloadProduct) | List of purchased downloadable items + +### CustomerDownloadableProduct object {#custDownloadProduct} + +The `CustomerDownloadableProduct` object contains the following attributes: + +Attribute | Type | Description +--- | --- | --- +`date` | String | The date and time the purchase was made +`download_url` | String | The fully qualified URL to the download file +`order_increment_id` | String | The purchase order ID +`remaining_downloads` | String | Determines the number of times the customer can download the product +`status` | String | Determines the stage in the order workflow when the download becomes available. Options are `Pending` and `Invoiced` + +## Errors + +Error | Description +--- | --- +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. diff --git a/src/guides/v2.3/graphql/queries/customer-orders.md b/src/guides/v2.3/graphql/queries/customer-orders.md new file mode 100644 index 00000000000..abc41cc51bb --- /dev/null +++ b/src/guides/v2.3/graphql/queries/customer-orders.md @@ -0,0 +1,88 @@ +--- +group: graphql +title: customerOrders query +redirect_from: + - /guides/v2.3/graphql/reference/sales.html +--- + +The Sales module performs a wide variety of functions, including order, invoice, and shipment management. However, most of these functions are performed on the backend, and the customer does not have access to this information. By returning a list of customer orders, the `customerOrders` query allows a customer to retrieve their order histories. + +We recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +`{customerOrders {CustomerOrders}}` + +## Example usage + +The following query returns the order history of the logged in customer. + +**Request:** + +```graphql +{ + customerOrders { + items { + order_number + id + created_at + grand_total + status + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "customerOrders": { + "items": [ + { + "order_number": "000000001", + "id": 1, + "created_at": "2019-02-21 00:24:34", + "grand_total": 36.39, + "status": "processing" + }, + { + "order_number": "000000002", + "id": 2, + "created_at": "2019-02-21 00:24:35", + "grand_total": 39.64, + "status": "closed" + } + ] + } + } +} +``` + +## Output attributes + +The `CustomerOrders` object contains the `items` attribute. + +Attribute | Data type | Description +--- | --- | --- +`items` | [`[CustomerOrder]`](#customerOrderAttributes) | An array of customer orders + +### Customer order items attributes {#customerOrderAttributes} + +The `CustomerOrder` object defines details about each order the customer has placed. + +Attribute | Data type | Description +--- | --- | --- +`created_at` | String | A timestamp indicating when the order was placed +`grand_total` | Float | The total of the order +`id` | Int | The ID assigned to the customer's order +`increment_id` | String | Deprecated. Use `order_number` instead. An ID that indicates the sequence of the order in the customer's order history +`order_number` | String! | The order number assigned to the order +`status` | String | The status of the order, such as `open`, `processing`, or `closed` + +## Errors + +Error | Description +--- | --- +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. diff --git a/src/guides/v2.3/graphql/queries/customer-payment-tokens.md b/src/guides/v2.3/graphql/queries/customer-payment-tokens.md new file mode 100644 index 00000000000..4ed4f90cce7 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/customer-payment-tokens.md @@ -0,0 +1,75 @@ +--- +group: graphql +title: customerPaymentTokens query +redirect_from: + - /guides/v2.3/graphql/reference/vault.html +--- + +When the [vault]({{page.baseurl}}/payments-integrations/vault/vault-intro.html) feature is supported by a payment integration and enabled, customers have the option during checkout to save their credit card information. (Braintree supports the vault feature. Third-party payment integrations may support this feature as well.) During subsequent checkouts, the customer is presented with a list of saved payment options. If Instant Purchase is enabled, customers can even by-pass the two-step checkout process and place the order from the product page. + +The `customerPaymentTokens` query returns an array of stored payment methods. Use the [deletePaymentToken mutation]({{page.baseurl}}/graphql/mutations/delete-payment-token.html) to delete a payment token from the system. + +{:.bs-callout-info} +You must specify the customer's authorization token in the header of the call. + +## Syntax + +`{customerPaymentTokens{CustomerPaymentTokens}}` + +## Example usage + +The following example returns all the current customer's payment tokens. The `public_hash` output values will be unique to your application. + +**Request:** + +```graphql +query { + customerPaymentTokens { + items { + details + public_hash + payment_method_code + type + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "customerPaymentTokens": { + "items": [ + { + "details": "{\"type\":\"VI\",\"maskedCC\":\"1111\",\"expirationDate\":\"09\\/2022\"}", + "public_hash": "377c1514e0...", + "payment_method_code": "braintree", + "type": "card" + }, + { + "details": "{\"type\":\"DI\",\"maskedCC\":\"1117\",\"expirationDate\":\"11\\/2023\"}", + "public_hash": "f5816fe2ab...", + "payment_method_code": "braintree", + "type": "card" + } + ] + } + } +} +``` + +## Output attributes + +{% include graphql/customer-payment-tokens.md %} + +## Errors + +Error | Description +--- | --- +`The current customer isn't authorized.` | The current customer is not currently logged in, or the customer's token does not exist in the `oauth_token` table. + +## Related topics + +[deletePaymentToken mutation]({{page.baseurl}}/graphql/mutations/delete-payment-token.html) diff --git a/src/guides/v2.3/graphql/queries/customer.md b/src/guides/v2.3/graphql/queries/customer.md new file mode 100644 index 00000000000..3997bbe7639 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/customer.md @@ -0,0 +1,338 @@ +--- +group: graphql +title: customer query +redirect_from: + - /guides/v2.3/graphql/reference/customer.html +--- + +The `customer` query returns information about the logged-in customer, store credit history and customer's wishlist. + +To return or modify information about a customer, we recommend you use customer tokens in the header of your GraphQL calls. However, you also can use [session authentication]({{ page.baseurl }}/get-started/authentication/gs-authentication-session.html). + +## Syntax + +`{customer: {Customer}}` + +## Example usage + +### Retrieving the logged-in customer + +The following call returns information about the logged-in customer. Provide the customer's token in the header section of the query. + +**Request:** + +```graphql +{ + customer { + firstname + lastname + suffix + email + addresses { + firstname + lastname + street + city + region { + region_code + region + } + postcode + country_code + telephone + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "customer": { + "firstname": "John", + "lastname": "Doe", + "suffix": null, + "email": "jdoe@example.com", + "addresses": [ + { + "firstname": "John", + "lastname": "Doe", + "street": [ + "123 Elm Street" + ], + "city": "Anytown", + "region": { + "region_code": "MI", + "region": "Michigan" + }, + "postcode": "78758", + "country_code": "US", + "telephone": "512 555-1212" + } + ] + } + } +} +``` + +### Retrieving the store credit history + +The following example returns the store credit history for the logged-in user. + +**Request:** + +```graphql +query { + customer { + firstname + lastname + store_credit { + enabled + balance_history(pageSize: 10) { + items { + action + actual_balance { + currency + value + } + balance_change { + currency + value + } + date_time_changed + } + page_info { + page_size + current_page + total_pages + } + total_count + } + current_balance { + currency + value + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "customer": { + "firstname": "John", + "lastname": "Doe", + "store_credit": { + "enabled": true, + "balance_history": { + "items": [ + { + "action": "Updated", + "actual_balance": { + "currency": "USD", + "value": 10 + }, + "balance_change": { + "currency": "USD", + "value": -100 + }, + "date_time_changed": "2019-07-15 21:47:59" + }, + { + "action": "Updated", + "actual_balance": { + "currency": "USD", + "value": 110 + }, + "balance_change": { + "currency": "USD", + "value": 10 + }, + "date_time_changed": "2019-07-15 21:47:18" + }, + { + "action": "Created", + "actual_balance": { + "currency": "USD", + "value": 100 + }, + "balance_change": { + "currency": "USD", + "value": 100 + }, + "date_time_changed": "2019-07-15 16:31:05" + } + ], + "page_info": { + "page_size": 10, + "current_page": 1, + "total_pages": 1 + }, + "total_count": 3 + }, + "current_balance": { + "currency": "USD", + "value": 10 + } + } + } + } +} +``` +### Retrieve the customer's wish list + +The following query returns the customer's wish list: + +**Request:** + +```graphql +{ + customer { + wishlist { + items { + id + description + qty + product { + sku + name + price_range { + maximum_price { + regular_price { + value + } + } + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "customer": { + "wishlist": { + "items": [ + { + "id": 1, + "description": "I need this", + "qty": 1, + "product": { + "sku": "24-WG080", + "name": "Sprite Yoga Companion Kit", + "price_range": { + "maximum_price": { + "regular_price": { + "value": 77 + } + } + } + } + }, + { + "id": 2, + "description": null, + "qty": 1, + "product": { + "sku": "24-UG04", + "name": "Zing Jump Rope", + "price_range": { + "maximum_price": { + "regular_price": { + "value": 12 + } + } + } + } + } + ] + } + } + } +} +``` +## Output attributes + +### Customer attributes {#customerAttributes} + +The `customer` object can contain the following attributes: + +{% include graphql/customer-output.md %} + +### Wishlist attributes {#Wishlist} + +Attribute | Data type | Description +--- | --- | --- +`items` | [[WishlistItem]](#wishlistitem) | An array of items in the customer's wish list +`items_count` | Int | The number of items in the wish list +`id` | ID | The unique identifier of the wish list +`sharing_code` | String | An encrypted code that Magento uses to link to the wish list +`updated_at` | String | The time of the last modification to the wish list + +### WishlistItem attributes {#wishlistitem} + +Attribute | Data type | Description +--- | --- | --- +`added_at` | String | The time when the customer added the item to the wish list +`description` | String | The customer's comment about this item +`id` | Int | The wish list item ID +`product` | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html) | The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes +`qty` | Float | The quantity of this wish list item + +### Store credit attributes + +In {{site.data.var.ee}}, the merchant can assign store credit to customers. Magento maintains the history of all changes to the balance of store credit available to the customer. The customer must be logged in to access the store credit history and balance. + +Store credits must be enabled to return store credit attributes. If store credits are disabled after previously being enabled, the query returns the customer's current balance as null. + +Attribute | Data Type | Description +--- | --- | --- +`store_credit` | [CustomerStoreCredit](#CustomerStoreCredit) | Contains the store credit information for the logged-in customer + +### CustomerStoreCredit attributes {#CustomerStoreCredit} + +The `store_credit` object contains store credit information, including the balance and history. + +Attribute | Data Type | Description +--- | --- | --- +`balance_history` | [`CustomerStoreCreditHistory`](#CustomerStoreCreditHistory) | Lists changes to the amount of store credit available to the customer. If the history or store credit feature is disabled, then a null value will be returned.

    You can specify the following optional parameters to control paging in the output.

    `pageSize` - An integer that specifies the maximum number of results to return at once. The default value is 20.

    `currentPage` - An integer that specifies which page of the results to return. The default value is 1 +`current_balance` | Money | The current store credit balance +`enabled` | Boolean | Indicates whether store credits are enabled. If the feature is disabled, then the balance will not be returned + +### CustomerStoreCreditHistory attributes {#CustomerStoreCreditHistory} + +The `CustomerStoreCreditHistory` object contains an array of store credit items and paging information. If the store credit or store credit history feature is disabled, then a null value will be returned. + +Attribute | Data Type | Description +--- | --- | --- +`items` | [[`CustomerStoreCreditHistoryItem`](#CustomerStoreCreditHistoryItem)] | An array of products that match the specified search criteria +`page_info` | SearchResultPageInfo | An object that includes the `page_size` and `current_page` values specified in the query +`total_count` | Int | The number of items returned + +### CustomerStoreCreditHistoryItem attributes {#CustomerStoreCreditHistoryItem} + +The `CustomerStoreCreditHistoryItem` object contains information about a specific change to the customer's store credit. + +Attribute | Data Type | Description +--- | --- | --- +`action` | String | The action taken on the customer's store credit +`actual_balance` | Money | The store credit available to the customer as a result of this action +`balance_change` | Money | The amount added to or subtracted from the store credit as a result of this action +`date_time_changed` | String | Date and time when the store credit change was made + +## Related topics + +* [isEmailAvailable query]({{page.baseurl}}/graphql/queries/is-email-available.html) +* [generateCustomerToken mutation]({{page.baseurl}}/graphql/mutations/generate-customer-token.html) +* [createCustomer mutation]({{page.baseurl}}/graphql/mutations/create-customer.html) +* [createCustomerAddress mutation]({{page.baseurl}}/graphql/mutations/create-customer-address.html) diff --git a/src/guides/v2.3/graphql/queries/directory-countries.md b/src/guides/v2.3/graphql/queries/directory-countries.md new file mode 100644 index 00000000000..d85c5d688eb --- /dev/null +++ b/src/guides/v2.3/graphql/queries/directory-countries.md @@ -0,0 +1,145 @@ +--- +group: graphql +title: countries query +--- + +The `countries` query returns all countries in which the entity can do business. + +Use the [country]({{page.baseurl}}/graphql/queries/directory-country.html) query if you want to retrieve information about a specific country. + +## Syntax + +`{countries {Countries}}` + +## Example usage + +The following query returns all countries listed for the current instance of Magento: + +**Request:** + +```graphql +query { + countries { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +``` + +**Response:** + +In this example, the response is intentionally truncated. The `available_regions` attribute value will be null if the country does not have any regions available. Otherwise, it contains an array of the country's regions. + +```json +{ + "data": { + "countries": [ + { + "id": "AD", + "two_letter_abbreviation": "AD", + "three_letter_abbreviation": "AND", + "full_name_locale": "Andorra", + "full_name_english": "Andorra", + "available_regions": null + }, + { + "id": "AE", + "two_letter_abbreviation": "AE", + "three_letter_abbreviation": "ARE", + "full_name_locale": "United Arab Emirates", + "full_name_english": "United Arab Emirates", + "available_regions": null + }, + { + "id": "AF", + "two_letter_abbreviation": "AF", + "three_letter_abbreviation": "AFG", + "full_name_locale": "Afghanistan", + "full_name_english": "Afghanistan", + "available_regions": null + }, + { + "id": "AG", + "two_letter_abbreviation": "AG", + "three_letter_abbreviation": "ATG", + "full_name_locale": "Antigua and Barbuda", + "full_name_english": "Antigua and Barbuda", + "available_regions": null + }, + { + "id": "AT", + "two_letter_abbreviation": "AT", + "three_letter_abbreviation": "AUT", + "full_name_locale": "Austria", + "full_name_english": "Austria", + "available_regions": [ + { + "id": 102, + "code": "BL", + "name": "Burgenland" + }, + { + "id": 99, + "code": "KN", + "name": "Kärnten" + }, + { + "id": 96, + "code": "NO", + "name": "Niederösterreich" + }, + { + "id": 97, + "code": "OO", + "name": "Oberösterreich" + }, + { + "id": 98, + "code": "SB", + "name": "Salzburg" + }, + { + "id": 100, + "code": "ST", + "name": "Steiermark" + }, + { + "id": 101, + "code": "TI", + "name": "Tirol" + }, + { + "id": 103, + "code": "VB", + "name": "Vorarlberg" + }, + { + "id": 95, + "code": "WI", + "name": "Wien" + } + ] + } + ] + } +} +``` + +## Output attributes + +The query returns an array of `Country` objects. + +{% include graphql/country-output.md %} + +## Related topics + +* [country query]({{page.baseurl}}/graphql/queries/directory-country.html) +* [currency query]({{page.baseurl}}/graphql/queries/directory-currency.html) diff --git a/src/guides/v2.3/graphql/queries/directory-country.md b/src/guides/v2.3/graphql/queries/directory-country.md new file mode 100644 index 00000000000..f9a26b67cb2 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/directory-country.md @@ -0,0 +1,121 @@ +--- +group: graphql +title: country query +redirect_from: + - /guides/v2.3/graphql/reference/directory.html +--- + +Use the `country` query to retrieve information about a specific country. + +Use the [countries]({{page.baseurl}}/graphql/queries/directory-countries.html) query to retrieve a list of countries available in the system. + +## Syntax + +`{country(id: String) {Country}}` + +## Example usage + +The following query uses a two-letter abbreviation for the country ID (id: "AU"), which returns information about Australia. + +**Request:** + +```graphql +query { + country(id: "AU") { + id + two_letter_abbreviation + three_letter_abbreviation + full_name_locale + full_name_english + available_regions { + id + code + name + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "country": { + "id": "AU", + "two_letter_abbreviation": "AU", + "three_letter_abbreviation": "AUS", + "full_name_locale": "Australia", + "full_name_english": "Australia", + "available_regions": [ + { + "id": 569, + "code": "ACT", + "name": "Australian Capital Territory" + }, + { + "id": 570, + "code": "NSW", + "name": "New South Wales" + }, + { + "id": 576, + "code": "NT", + "name": "Northern Territory" + }, + { + "id": 572, + "code": "QLD", + "name": "Queensland" + }, + { + "id": 573, + "code": "SA", + "name": "South Australia" + }, + { + "id": 574, + "code": "TAS", + "name": "Tasmania" + }, + { + "id": 571, + "code": "VIC", + "name": "Victoria" + }, + { + "id": 575, + "code": "WA", + "name": "Western Australia" + } + ] + } + } +} +``` + +## Input attributes + +The `country` query requires the following input: + +Attribute | Data type | Description +--- | --- | --- +`id` | String | A unique ID for the country + +## Output attributes + +The query returns a single `Country` object. + +{% include graphql/country-output.md %} + +## Related topics + +* [countries query]({{page.baseurl}}/graphql/queries/directory-countries.html) +* [currency query]({{page.baseurl}}/graphql/queries/directory-currency.html) + +## Errors + +Error | Description +--- | --- +`Country \"id\" value should be specified"` | The Country ID value must be specified to find the mapped country. +`The country isn't available` | There is no country mapped to the given country ID. diff --git a/src/guides/v2.3/graphql/queries/directory-currency.md b/src/guides/v2.3/graphql/queries/directory-currency.md new file mode 100644 index 00000000000..7fac059654b --- /dev/null +++ b/src/guides/v2.3/graphql/queries/directory-currency.md @@ -0,0 +1,88 @@ +--- +group: graphql +title: currency query +--- + +Use the `currency` query to return information about the store's currency configuration. + +## Syntax + +`{currency {Currency}}` + +## Example usage + +The following query returns currency information for an instance of Magento that is configured for multiple currencies, USD and EUR. The default (base) currency for the store is US Dollar (USD). The response includes a list of currencies in the `available_currency_codes` attribute as well as a set of exchange rates. + +**Request:** + +```graphql +query { + currency { + base_currency_code + base_currency_symbol + default_display_currency_code + default_display_currency_symbol + available_currency_codes + exchange_rates { + currency_to + rate + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "currency": { + "base_currency_code": "USD", + "base_currency_symbol": "$", + "default_display_currency_code": "USD", + "default_display_currency_symbol": "$", + "available_currency_codes": [ + "EUR", + "USD" + ], + "exchange_rates": [ + { + "currency_to": "EUR", + "rate": 0.7067 + }, + { + "currency_to": "USD", + "rate": 1 + } + ] + } + } +} +``` + +## Output attributes + +The `currency` object provides the following attributes: + +Attribute | Data type | Description +--- | --- | --- +`available_currency_codes` | [String] | An array of three-letter currency codes accepted by the store, such as `USD` and `EUR` +`base_currency_code` | String | The base currency set for the store, such as USD +`base_currency_symbol` | String | The symbol for the specified base currency, such as $ +`default_display_currency_code` | String | Specifies if the currency code is set as the store's default +`default_display_currency_symbol` | String | Specifies if the currency symbol is set as the store's default +`exchange_rates` | [[ExchangeRate]](#exchangeRateAttributes) | An array of exchange rates specified in the store + +## Exchange rate attributes {#exchangeRateAttributes} + +The `ExchangeRate` object provides the following attributes: + +Attribute | Data type | Description +--- | --- | --- +`currency_to` | String | Specifies the store's default currency to exchange to +`rate` | Float | The exchange rate for the store's default currency + +## Related topics + +* [countries query]({{page.baseurl}}/graphql/queries/directory-countries.html) +* [country query]({{page.baseurl}}/graphql/queries/directory-country.html) diff --git a/src/guides/v2.3/graphql/queries/get-hosted-pro-url.md b/src/guides/v2.3/graphql/queries/get-hosted-pro-url.md new file mode 100644 index 00000000000..f04f8d07374 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/get-hosted-pro-url.md @@ -0,0 +1,60 @@ +--- +group: graphql +title: getHostedProUrl query +--- + +The `getHostedProUrl` query is required to complete a transaction when the [PayPal Website Payments Pro Hosted Solution payment method]({{page.baseurl}}/graphql/payment-methods/hosted-pro.html) is selected. The query retrieves a PayPal-generated URL that the PWA client connects to, enabling the customer to enter their PayPal credentials and complete the transaction. Run this query after you [set the payment method]({{ page.baseurl}}/graphql/mutations/set-payment-method.html) and [place the order]({{ page.baseurl}}/graphql/mutations/place-order.html). + +## Syntax + +`getHostedProUrl(input: HostedProUrlInput!): HostedProUrl` + +## Example usage + +The following query returns the secure URL generated by PayPal: + +**Request:** + +```graphql +query { + getHostedProUrl(input: { cart_id: "mwqoyxgbibvgkr3udszfzomxpoj2gmj6" }) { + secure_form_url + } +} +``` + +**Response:** + +```json +{ + "data": { + "getHostedProUrl": { + "secure_form_url": "https://securepayments.sandbox.paypal.com/webapps/HostedSoleSolutionApp/webflow/sparta/hostedSoleSolutionProcess?hosted_button_id=HSSS-iKGrv2XMlHcGGj8u.hlOHA2AeoQHcIQOvoqTEbvgBlKTLXcS8tAg0BRg1AklvfIhU5ip0g" + } + } +} +``` + +## Input attributes + +The `getHostedProUrl` query must contain the following attribute: + +Attribute | Data type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer’s cart + +## Output attributes + +The query returns the PayPal URL that enables the customer to sign in to PayPal and complete the transaction. + +Attribute | Data type | Description +--- | --- | --- +`secure_form_url` | String | Secure URL generated by PayPal + +## Errors + +Error | Description +--- | --- +`Could not find a cart with ID \"xxxxx\"` | The ID provided in the `cart_id` field is invalid or the cart does not exist for the customer. +`The cart isn't active` | The cart with the specified cart ID is unavailable, because the items have been purchased and the cart ID becomes inactive. +`Field HostedProUrlInput.cart_id of required type String! was not provided` | The value specified in the `HostedProUrlInput.cart_id` argument is empty. \ No newline at end of file diff --git a/src/guides/v2.3/graphql/queries/get-payflow-link-token.md b/src/guides/v2.3/graphql/queries/get-payflow-link-token.md new file mode 100644 index 00000000000..5c27a09300e --- /dev/null +++ b/src/guides/v2.3/graphql/queries/get-payflow-link-token.md @@ -0,0 +1,73 @@ +--- +group: graphql +title: getPayflowLinkToken query +--- + +The `getPayflowLinkToken` query retrieves PayPal payment credentials for a PayPal Payflow transaction. You must run this query after you [set the payment method]({{ page.baseurl}}/graphql/mutations/set-payment-method.html) and [place the order]({{ page.baseurl}}/graphql/mutations/place-order.html). + +See [Paypal Payflow Link payment method]({{page.baseurl}}/graphql/payment-methods/payflow-link.html) for detailed information about the workflow of PayPal Payflow Link transactions. + +## Syntax + +`getPayflowLinkToken(input: PayflowLinkTokenInput): PayflowLinkToken` + +## Example + +The following example requests a token in a Payflow Link transaction. + +**Request:** + +```graphql +{ + getPayflowLinkToken(input: {cart_id: "123"}) { + secure_token + secure_token_id + mode + paypal_url + } +} +``` + +**Response:** + +```json +{ + "data": { + "getPayflowLinkToken": { + "secure_token": "", + "secure_token_id": "", + "mode": "TEST", + "paypal_url": "https://pilot-payflowlink.paypal.com" + } + } +} +``` + +## Input attributes + +### PayflowLinkTokenInput {#PayflowLinkTokenInput} + +The `PayflowLinkTokenInput` object defines the attributes required to receive a Payflow Link token from PayPal. + +Attribute | Data Type | Description +--- | --- | --- +`cart_id` | String! | The unique ID that identifies the customer's cart + +## Output attributes + +### PayflowLinkToken + +The `PayflowLinkToken` object contains a token returned by PayPal and a set of URLs that allow the buyer to authorize payment and adjust checkout details. + +Attribute | Data Type | Description +--- | --- | --- +`mode` | `PayflowLinkMode` | The mode for the Payflow Link payment. Must be `LIVE` (actual transaction) or `TEST` (sandbox transaction) +`paypal_url` | String | The PayPal URL used for requesting a Payflow form +`secure_token` | String | Secure token generated by PayPal +`secure_token_id` | String | Secure token ID generated by PayPal + +## Errors + +Error | Description +--- | --- +`No such entity with cartId` | An invalid `cartId` was provided diff --git a/src/guides/v2.3/graphql/queries/giftcard-account.md b/src/guides/v2.3/graphql/queries/giftcard-account.md new file mode 100644 index 00000000000..23b6102f766 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/giftcard-account.md @@ -0,0 +1,78 @@ +--- +group: graphql +title: giftCardAccount query +ee_only: True +redirect from: + - /guides/v2.3/graphql/reference/quote-giftcard-account.html +--- + +The `giftCardAccount` query returns information for a specific gift card. + +## Syntax + + `giftCardAccount(code: String!): GiftCardAccount` + +## Example usage + +The following example returns information about the `01PNC9L76H4H` gift card code. + +**Request:** + +```graphql +query { + giftCardAccount(input: {gift_card_code: "01PNC9L76H4H"}){ + code + balance { + currency + value + } + expiration_date + } +} +``` + +**Response:** + +```json +{ + "data": { + "giftCardAccount": { + "code": "01PNC9L76H4H", + "balance": { + "currency": "USD", + "value": 25 + }, + "expiration_date": null + } + } +} +``` + +## Input attributes + +The `giftCardAccount` query requires the `gift_card_code`. + +### GiftCardAccount object {#GiftCardAccount} + +The `GiftCardAccount` object must contain the following attribute: + +Attribute | Data Type | Description +--- | --- | --- +`gift_card_code` | String! | The gift card code + +## Output attributes + +The `GiftCardAccount` object returns the following attributes: + +Attribute | Data Type | Description +--- | --- | --- +`balance` | Money | Returns the currency and remaining balance of the gift card +`code` | String | Returns the gift card code +`expiration_date` | String | Returns the date when the gift card expires, if any + +## Errors + +Error | Description +--- | --- +`Gift card not found` | The specified `gift_card_code` value does not exist in the `giftcardaccount` table, or the full amount has already been redeemed. +`Field GiftCardAccountInput.gift_card_code of required type String! was not provided` | The value specified in the `GiftCardAccountInput.gift_card_code` argument is empty. diff --git a/src/guides/v2.3/graphql/queries/index.md b/src/guides/v2.3/graphql/queries/index.md new file mode 100644 index 00000000000..2f885e57a0e --- /dev/null +++ b/src/guides/v2.3/graphql/queries/index.md @@ -0,0 +1,577 @@ +--- +group: graphql +title: Using queries +redirect_from: + - /guides/v2.3/graphql/search-pagination.html + - /guides/v2.3/graphql/queries.html +--- + +A GraphQL query retrieves data from the Magento server in a similar manner as a REST GET call. The current set of Magento GraphQL queries allow a mobile app or browser to render a wide variety of information, including the following: + +* A set of products to be displayed. This can include the entire catalog or those that match customer-specified criteria. +* Customer data. With a customer token, a query can retrieve basic information about a customer as well as billing and shipping addresses, wish lists, order history, and other sensitive data. +* Shopping cart contents. GraphQL supports both guest and logged-in customer carts. +* Store configuration values, including theme and CMS settings, the currency code, and supported countries. + +The Magento REST GET endpoints retrieve a wide variety of information on behalf of the merchant. Many of these endpoints are for retrieving backend information. For example, the `GET /V1/customers/search` endpoint can be used to find a subset of customers that meet certain criteria, such as those that live in a particular state or have a birthday this month. Likewise, the `GET /V1/invoices` endpoint can return all the recently-generated invoices. This type of functionality is not required for the frontend, so it is not available in GraphQL queries. The queries are designed to improve the customer's user experience by quickly retrieving the data needed to render pages. + +Over time, the Magento GraphQL queries will duplicate the functionality of all storefront-facing GET calls, while making it possible to query more data in one request. The main difference will be that GraphQL will support storefront use cases, while REST will support admin use cases. + +## Structure of a query + +A query contains the following elements: + +* The optional keyword `query`. If no keyword is specified at the beginning of a request, the processor assumes the request is a query. +* An operation name for your local implementation. This name is required if you include variables. Otherwise, it is optional. +* The query name +* The terms to search for. The terms can be in the form of objects, attributes, or a combination. Queries that don't require search terms obtain their context from the customer's authorization token or store ID, both of which are specified in the header of the call. +* The output object, which specifies which data the query returns. + +The following example shows the structure of the `cart` query: + +```graphql +query myCartQuery{ + cart(cart_id: String!): Cart +} +``` + +In the preceding example, `myCartQuery` identifies your implementation of the `cart` query. `cart_id` is a non-nullable string that defines the cart to query. (The exclamation point indicates the value is non-nullable.) The `Cart` output object defines which fields to return. + +Now let's fully define a query: + +```graphql +query myCartQuery{ + cart(cart_id: "1WxKm8WUm3uFKXLlHXezew5WREfVRPAn") { + items { + id + quantity + } + billing_address { + firstname + lastname + postcode + } + shipping_addresses { + firstname + lastname + postcode + } + } +} +``` + +In this example, we've supplied a cart ID as input, (which was generated by the `createEmptyCart` mutation). The output includes the `cart_id` as well as selected information about the items in the cart and the billing and shipping addresses. + +The following example shows the query response: + +```json +{ + "data": { + "cart": { + "items": [ + { + "id": "5", + "quantity": 1 + } + ], + "billing_address": { + "firstname": "Veronica", + "lastname": "Costello", + "postcode": "49628-7978" + }, + "shipping_addresses": [ + { + "firstname": "Veronica", + "lastname": "Costello", + "postcode": "49628-7978" + } + ] + } + } +} +``` + +{:.bs-callout-tip} +Magento will not run a query that is too complex. The number of fields, objects, and nodes are factors in determining the complexity of a query. + +## Query variables + +Specifying variables in a query can help increase code re-use. Consider the following requirements when generating a query that contains one or more variables: + +* All variables must be declared up-front, immediately after the operation name. +* Variables are typed: they can be scalar or an object. +* You must use all declared variables. Object variables are defined in JSON. + +The following example declares the `$cart_id` variable. It is referenced in the `input` statement. + +```graphql +query myCartQueryWithVariable($cart_id: String!) { + cart(cart_id: $cart_id) { + items { + id + quantity + } + billing_address { + firstname + lastname + postcode + } + shipping_addresses { + firstname + lastname + postcode + } + } +} +``` + +Variables are defined separately in JSON: + +```json +{ + "cart_id": "1WxKm8WUm3uFKXLlHXezew5WREfVRPAn" +} +``` + +## Staging queries {#staging} + +Magento GraphQL allows you to use certain queries to return preview information for staged content. Staging, a {{site.data.var.ee}} feature, allows merchants to schedule a set of changes to the storefront that run for a prescribed time in the future. These changes, also known as a _campaign_, are defined within the Admin. Customers do not have access to staged content, and as a result, staging queries have requirements that do not apply to traditional queries and mutations. + +[Content Staging](https://docs.magento.com/m2/ee/user_guide/cms/content-staging.html) in the _Merchant User Guide_ describes how to create a campaign. + +You can use the following queries to return staged preview information. + +* `categoryList` +* `products` + +{:.bs-callout-info} +The `products` query does not support full text search in the context of staging, because staged content is not indexed. Therefore, omit the `search` input attribute in your staging `products` queries. + +A staging query requires two specialized headers: + +Header name | Description +--- | --- +`Authorization Bearer: ` | An admin token. Use the `POST /V1/integration/admin/token` REST endpoint to generate this token. +`Preview-Version` | A timestamp (seconds since January 1, 1970) that is inside the range of dates of the campaign you are querying. + +Magento returns an authorization error if you specify an invalid token or do not include both headers. If the specified timestamp does not correspond to a date in a scheduled campaign, the query results reflect the current storefront settings. + +Magento also returns an error if you specify these headers with any other query or any mutation. + +### Example campaign + +The example staging queries in this section are based on a simple campaign that creates a custom category and catalog sales rule using the Luma sample data. By default, the custom category and sales rule are disabled but become enabled when the campaign takes effect. + +The following steps describe how to create this example campaign. + +1. Create a subcategory of **Sale** named **End of Year Sale**. Set the **Enable Category** field to **No**. +1. Add several products to the subcategory. +1. Schedule an update named **End of Year Sale Update** for the subcategory that takes effect at a later date. Configure the update so that the **Enable Category** field is set to **Yes**. +1. Create a catalog sales rule with the following properties: + * Set the **Active** switch to **No**. + * In the **Conditions** section, define the condition as **Category is **. + * In the **Actions** section, set the **Apply** field to **Apply a percentage of original** and the **Discount Amount** field to **25**. +1. Schedule an update for the catalog sales rule and assign it to the **End of Year Sale Update**. In this update, set the **Active** switch to **Yes**. + +#### Staging `products` query + +The following query returns information about a product (`24-UG05`) in the **End of Year Sale** campaign. The `Preview-Version` header contains the timestamp for a date that is within the duration of the campaign. When you include the proper headers, the query returns prices with applied discounts. Without the headers, the query returns only default prices. + +**Headers:** + +```text +Authorization: Bearer hoyz7k697ubv5hcpq92yrtx39i7x10um +Preview-Version: 1576389600 +``` + +**Request:** + +```graphql +{ + products(filter: {sku: {eq: "24-UG05"}}) { + items { + name + sku + price_range { + minimum_price { + discount { + percent_off + amount_off + } + final_price { + value + currency + } + regular_price { + value + } + } + } + } + } +} +``` + +**Response with headers:** + +```json +{ + "data": { + "products": { + "items": [ + { + "name": "Go-Get'r Pushup Grips", + "sku": "24-UG05", + "price_range": { + "minimum_price": { + "discount": { + "percent_off": 25, + "amount_off": 4.75 + }, + "final_price": { + "value": 14.25, + "currency": "USD" + }, + "regular_price": { + "value": 19 + } + } + } + } + ] + } + } +} +``` + +**Response without headers:** + +```json +{ + "data": { + "products": { + "items": [ + { + "name": "Go-Get'r Pushup Grips", + "sku": "24-UG05", + "price_range": { + "minimum_price": { + "discount": { + "percent_off": 0, + "amount_off": 0 + }, + "final_price": { + "value": 19, + "currency": "USD" + }, + "regular_price": { + "value": 19 + } + } + } + } + ] + } + } +} +``` + +#### Staging `categoryList` query + +In this example campaign, the **End of Year Sale** subcategory and a catalog price rule are disabled when the campaign is not in effect. When you specify valid headers, the `categoryList`query returns full details about the custom category. Otherwise, the query returns an empty array. + +**Headers:** + +```text +Authorization: Bearer hoyz7k697ubv5hcpq92yrtx39i7x10um +Preview-Version: 1576389600 +``` + +**Request:** + +```graphql +{ + categoryList(filters: {ids: {eq: "43"}}) { + name + level + products( + sort: { + price: ASC + } + pageSize: 20 + currentPage: 1 + ) { + total_count + items { + name + sku + price_range { + minimum_price { + discount { + amount_off + percent_off + } + final_price { + value + } + regular_price { + value + } + } + } + } + } + } +} +``` + +**Response with headers:** + +```json +{ + "data": { + "categoryList": [ + { + "name": "End of Year Sale", + "level": 3, + "products": { + "total_count": 4, + "items": [ + { + "name": "Solo Power Circuit", + "sku": "240-LV07", + "price_range": { + "minimum_price": { + "discount": { + "amount_off": 3.5, + "percent_off": 25 + }, + "final_price": { + "value": 10.5 + }, + "regular_price": { + "value": 14 + } + } + } + }, + { + "name": "Quest Lumaflex™ Band", + "sku": "24-UG01", + "price_range": { + "minimum_price": { + "discount": { + "amount_off": 4.75, + "percent_off": 25 + }, + "final_price": { + "value": 14.25 + }, + "regular_price": { + "value": 19 + } + } + } + }, + { + "name": "Go-Get'r Pushup Grips", + "sku": "24-UG05", + "price_range": { + "minimum_price": { + "discount": { + "amount_off": 4.75, + "percent_off": 25 + }, + "final_price": { + "value": 14.25 + }, + "regular_price": { + "value": 19 + } + } + } + }, + { + "name": "Gabrielle Micro Sleeve Top", + "sku": "WS02", + "price_range": { + "minimum_price": { + "discount": { + "amount_off": 7.00, + "percent_off": 25 + }, + "final_price": { + "value": 21 + }, + "regular_price": { + "value": 28 + } + } + } + } + ] + } + } + ] + } +} +``` +**Response without headers:** + +```json +{ + "data": { + "categoryList": [] + } +} +``` + +## Introspection queries + +Introspection queries allow you to return information about the schema. For example, you might want a list of Magento GraphQL queries or details about a specific data type. The GraphQL specification determines the structure of introspection queries. See [Introspection](https://graphql.org/learn/introspection/) for more information. + +A Magento introspection query returns the same result whether or not you assign it an operation name, such as `IntrospectionQuery`. + +### Disable introspection querying + +Introspection querying is enabled by default. To disable it in production mode to improve security, add the following to your `app/etc/env.php` file. + +```php +'graphql' => [ + 'disable_introspection' => true, +] +``` + +### Example introspection queries + +#### Return a list of Magento queries + +The following query returns a list of Magento queries. + +**Request:** + +```graphql +query IntrospectionQuery { + __schema { + queryType { + fields { + name + description + type{ + name + kind + } + } + } + } +} +``` +#### Return a list of Magento mutations + +The following query returns a list of Magento mutations. + +**Request:** + +```graphql +query IntrospectionQuery { + __schema { + mutationType { + fields { + name + description + type{ + name + kind + } + } + } + } +} +``` +#### Get details about a data type + +The following introspection query returns details about the `ProductAttributeFilterInput` data type. + +**Request:** + +```graphql +query IntrospectionQuery { + __type(name: "ProductAttributeFilterInput") { + name + kind + description + inputFields { + name + description + defaultValue + } + fields { + name + args { + name + description + type { + kind + name + } + } + type { + kind + name + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "__type": { + "name": "ProductAttributeFilterInput", + "kind": "INPUT_OBJECT", + "description": "ProductAttributeFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.", + "inputFields": [ + { + "name": "category_id", + "description": "Filter product by category id", + "defaultValue": null + }, + { + "name": "description", + "description": "Attribute label: Description", + "defaultValue": null + }, + { + "name": "name", + "description": "Attribute label: Product Name", + "defaultValue": null + }, + { + "name": "price", + "description": "Attribute label: Price", + "defaultValue": null + }, + { + "name": "short_description", + "description": "Attribute label: Short Description", + "defaultValue": null + }, + { + "name": "sku", + "description": "Attribute label: SKU", + "defaultValue": null + }, + { + "name": "url_key", + "description": "The part of the URL that identifies the product", + "defaultValue": null + } + ], + "fields": null + } + } +} +``` \ No newline at end of file diff --git a/src/guides/v2.3/graphql/queries/is-email-available.md b/src/guides/v2.3/graphql/queries/is-email-available.md new file mode 100644 index 00000000000..7800f5ab967 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/is-email-available.md @@ -0,0 +1,59 @@ +--- +group: graphql +title: isEmailAvailable query +--- + +The `isEmailAvailable` query checks whether the specified email has already been used to create a customer account. A value of `true` indicates the email address is available, and the customer can use the email address to create an account. + +## Syntax + +`{isEmailAvailable (email): {IsEmailAvailableOutput}}` + +## Example usage + +The following example checks whether the email address `customer@example.com` is available to create a customer account. + +**Request:** + +```graphql +{ + isEmailAvailable(email: "customer@example.com") { + is_email_available + } +} +``` + +**Response:** + +```json +{ + "data": { + "isEmailAvailable": { + "is_email_available": true + } + } +} +``` + +## Input attribute + +Attribute | Data Type | Description +--- | --- | --- +`email` | String! | The email address to check + +## Output attribute + +Attribute | Data Type | Description +--- | --- | --- +`is_email_available` | Boolean | A value of `true` indicates the email address is available, and the customer can use the email address to create an account + +## Related topics + +[customer query]({{page.baseurl}}/graphql/queries/customer.html) + +## Errors + +Error | Description +--- | --- +`Email is invalid` | The given email-id is not in a proper format. +`Field isEmailAvailable.email of required type String! was not provided` | The value specified in the `isEmailAvailable.email` argument is empty. diff --git a/src/guides/v2.3/graphql/queries/products.md b/src/guides/v2.3/graphql/queries/products.md new file mode 100644 index 00000000000..6f0a3a5fb91 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/products.md @@ -0,0 +1,1364 @@ +--- +group: graphql +title: products query +redirect_from: + - /guides/v2.3/graphql/reference/products.html +--- + +The `products` query allows you to search for catalog items. + +## Syntax + +```graphql +products( + search: String + filter: ProductAttributeFilterInput + pageSize: Int + currentPage: Int + sort: ProductAttributeSortInput +): Products +``` + +## Input attributes + +Each query attribute is defined below: + +Attribute | Data type | Description +--- | --- | --- +`search` | String | Performs a full-text search using the specified key words +`filter` | ProductAttributeFilterInput | Identifies which attributes to search for and return. See [filter attribute](#ProductFilterInput) object for more information +`pageSize` | Int | Specifies the maximum number of results to return at once. The default value is 20 +`currentPage` | Int | Specifies which page of results to return. The default value is 1 +`sort` | ProductAttributeSortInput | Specifies which attribute to sort on, and whether to return the results in ascending or descending order + +### search attribute + +The `search` attribute causes Magento to perform a full text search on the specified keywords. This is the same type of search that is performed from the storefront. If multiple keywords are specified, each keyword is evaluated separately. + +Each query must contain a `search` or `filter` attribute, or both. + +### filter attribute {#ProductFilterInput} + +The `ProductAttributeFilterInput` object determines which attributes will be used to narrow the results in a `products` query. A filter contains at least one attribute, a comparison operator, and the value that is being searched for. The following example filter searches for products that have a `name` that contains the string `Bag` with a `price` that's less than or equal to `40`. + +```graphql +filter: { + name: { + match: "Bag" + } + price: { + to: "40" + } +} +``` + +Magento processes the attribute values specified in a `ProductAttributeFilterInput` as simple data types (strings, integers, Booleans). However, returned attributes can be a different, complex data type. For example, in a response, `price` is an object that contains a monetary value and a currency code. + +By default, you can use the following attributes as filters. To define a custom filter, see [Filtering with custom attributes]({{page.baseurl}}/graphql/custom-filters.html). Use the `input_type` output attribute of the [`customAttributeMetadata` query]({{page.baseurl}}/graphql/queries/custom-attribute-metadata.html) to determine whether your custom filter should include the `FilterEqualTypeInput`, `FilterMatchTypeInput`, or `FilterRangeTypeInput` data type. + +Attribute | Data type | Description +--- | --- | --- +`category_id` | FilterEqualTypeInput | Filters by category ID +`description` | FilterMatchTypeInput | Filters on the Description attribute +`name` | FilterMatchTypeInput | Filters on the Product Name attribute +`price` | FilterRangeTypeInput | Filters on the Price attribute +`short_description` | FilterMatchTypeInput | Filters on the Short Description attribute +`sku` | FilterEqualTypeInput | Filters on the SKU attribute +`url_key` | FilterEqualTypeInput | The part of the URL that identifies the product + +#### FilterEqualTypeInput attributes + +The `category_id`, `sku`, and `url_key` filters require a `FilterEqualTypeInput` object as input. You must specify a `FilterEqualTypeInput` object to filter on a custom product attribute of the following types: + +- Boolean +- Select +- Multiple select + +Attribute | Data type | Description +--- | --- | --- +`eq` | String | Use this attribute to exactly match the specified string. For example, to filter on a specific category ID, specify a value like `5` +`in` | [String] | Use this attribute to filter on an array of values. For example, to filter on category IDs 4, 5, and 6, specify a value of `["4", "5", "6"]` + +#### FilterMatchTypeInput attributes + +Use the `FilterMatchTypeInput` object to construct a filter that returns products that partially fuzzy match a string or contain the specified pattern. + +Attribute | Data type | Description +--- | --- | --- +`match` | String | Use this attribute to partially fuzzy match the specified string. For example, to filter on a specific SKU, specify a value such as `24-MB01` + +You must specify a `FilterMatchTypeInput` object to filter on a custom product attribute of the following types: + +- Text field +- Text area +- Any other type not explicitly listed in `FilterEqualTypeInput`, `FilterMatchTypeInput`, or `FilterRangeTypeInput` + +#### FilterRangeTypeInput attributes + +Use the `FilterRangeTypeInput` object to construct a filter that returns products that fall within a range of prices or dates. + +Attribute | Data type | Description +--- | --- | --- +`from` | String | Use this attribute to specify the lowest possible value in the range +`to` | String | Use this attribute to specify the highest possible value in the range + +### pageSize attribute {#pageSize} + +Magento's GraphQL implementation of pagination uses offsets so that it operates in the same manner as REST and SOAP API requests. + +The `pageSize` attribute specifies the maximum number of items to return. If no value is specified, 20 items are returned. + +### currentPage attribute + +The `currentPage` attribute specifies which page of results to return. If no value is specified, the first page is returned. Magento returns an error if you specify a value that is greater than the number of available pages. + +### sort attribute + +The `sort` attribute allows you to specify which field or fields to use for sorting the results. If you specify more than one field, Magento sorts by the first field listed. Then, if any items have the same value, those items will be sorted by the secondary field. The value for each field can be set to either `ASC` or `DESC`. + +If you do not specify a `sort` object, Magento sorts as follows: + +- If you specify the `search` attribute, the query sorts by relevance, in descending order. +- If you specify the `filter` attribute without specifying the `search` attribute, the query sorts by position, in ascending order. + +In previous versions, the `sort` attribute required a `ProductSortInput` object as input. The `sort` attribute now requires a `ProductAttributeSortInput` object, which can contain the following attributes: + +Attribute | Data type | Description +--- | --- | --- +`name` | SortEnum | Sorts by Product Name +`position` | SortEnum | Sorts by the position of products +`price` | SortEnum | Sorts by Price +`relevance` | SortEnum | (Default) Sorts by the search relevance score + +{:.bs-callout-info} +If you use MySQL for searches and you specify `relevance` and another sorting attribute, the `relevance` results are always listed first. This limitation does not apply to [Elasticsearch]({{page.baseurl}}/config-guide/elasticsearch/configure-magento.html). + +To enable sorting by an attribute that is not in the `ProductAttributeSortInput` object, set the **Stores** > Attributes > **Product** > > **Storefront Properties** > **Use in Search** and **Used in Sorting in Product Listing** fields to Yes. + +## Deprecated input attributes + +The `filter` and `sort` attributes require new input objects. The following sections list the deprecated attributes. + +### ProductFilterInput attributes + +The `filter` attribute previously required a `ProductFilterInput` object as input. This object has been deprecated. The replacement input object, `ProductAttributeFilterInput` is more restrictive about what attributes can be used in a `products` query by default. The following attributes can no longer be used in default filters. See [Filtering with custom attributes]({{page.baseurl}}/graphql/custom-filters.html) for more information. + +```text +country_of_manufacture +created_at +custom_layout +custom_layout_update +gift_message_available +has_options +image +image_label +manufacturer +max_price +meta_description +meta_keyword +meta_title +min_price +news_from_date +news_to_date +options_container +or +required_options +small_image +small_image_label +special_from_date +special_price +special_to_date +swatch_image +thumbnail +thumbnail_label +tier_price +updated_at +url_key +url_path +weight +``` + +{:.bs-callout-info} +The `or` attribute cannot be used in a `products` query. Logical OR searches are no longer supported. + +- `or` - The keyword required to perform a logical OR comparison. +- `news_from_date` - This attribute is transformed to `new_from_date` in a response. +- `news_to_date` - This attribute is transformed to `new_to_date` in a response. + +The following condition types have been deprecated: + +```text +from +gt +gteq +like +lt +lteq +moreq +neq +nin +nlike +notnull +null +to +``` + +{:.bs-callout-info} +Wildcards are no longer supported in `products` queries. + +### ProductSortInput attributes + +The following sorting attributes have been deprecated: + +```text +country_of_manufacture +created_at +custom_layout_update +custom_layout +description +gift_message_available +has_options +image_label +image +manufacturer +meta_description +meta_keyword +meta_title +news_from_date +news_to_date +options_container +required_options +short_description +sku +small_image_label +small_image +special_from_date +special_price +special_to_date +thumbnail_label +thumbnail +tier_price +updated_at +weight +``` + +## Output attributes {#Response} + +The query returns a `Products` object containing the following information: + +Attribute | Data type | Description +--- | --- | --- +`aggregations` | [[Aggregation]](#Aggregation) | Layered navigation aggregations +`filters` | LayerFilter | Deprecated. Use `aggregations` instead +`items` | [[ProductInterface]](#ProductInterface) | An array of products that match the specified search criteria +`page_info` | [SearchResultPageInfo](#SearchResultPageInfo) | An object that includes the `page_info` and `currentPage` values specified in the query +`sort_fields` | [SortFields](#SortFields) | An object that includes the default sort field and all available sort fields +`total_count` | Int | The number of products returned + +### Aggregation attributes {#Aggregation} + +Each aggregation within the `aggregations` object is a separate bucket that contains the attribute code and label for each filterable option (such as price, category ID, and custom attributes). It also includes the number of products within the filterable option that match the specified search criteria. + +{:.bs-callout-info} +To enable a custom attribute to return layered navigation and aggregation data from the Admin, set the **Stores** > Attributes > **Product** > > **Storefront Properties** > **Use in Layered Navigation** field to **Filterable (with results)** or **Filterable (no results)**. + +Attribute | Data type | Description +--- | --- | --- +`attribute_code` | String! | Attribute code of the filter item +`count` | Int | The number of filter items in the filter group +`label` | String | The filter name displayed in layered navigation +`options` | [AggregationOption] | Describes each aggregated filter option + +#### AggregationOption attributes {#AggregationOption} + +The `AggregationOption` array contains a list of possible options for the `attribute_code` defined in the aggregation. For example, if the `attribute_code` is `category_id`, the return options could include tops, bottoms, gear, and so on. + +Attribute | Data type | Description +--- | --- | --- +`count` | Int | The number of items returned by the filter +`label` | String | The label of the filter +`value` | String! | The internal ID representing the value of the option + +### ProductInterface attributes {#ProductInterface} + +The `items` object contains information about each product that match the search criteria. [ProductInterface]({{page.baseurl}}/graphql/interfaces/product-interface.html) describes the possible contents of this object. + +### SearchResultPageInfo attributes {#SearchResultPageInfo} + +The `SearchResultPageInfo` object provides navigation for the query response. + +Attribute | Data type | Description +--- | --- | --- +`current_page` | Int | Specifies which page of results to return +`page_size` | Int | Specifies the maximum number of items to return +`total_pages` | Int | The total number of pages returned + +### SortFields attributes {#SortFields} + +The `SortFields` object contains the default value for sort fields as well as all possible sort fields. + +Attribute | Type | Description +--- | --- | --- +`default` | String | The default sort field +`options` | [SortField] | An array that contains all the fields that can be used for sorting + +#### SortField attributes + +The `SortField` object contains a list of all the attributes that can be used to sort query results. + +Attribute | Type | Description +--- | --- | --- +`label` | String | The label of a sortable option +`value` | String | The attribute code of the sort field + +## Deprecated output attributes + +The `filters` output object has been deprecated in favor of the `aggregations` object. The following sections list the deprecated attributes. + +### LayerFilter object + +The `LayerFilter` object can be returned in a response to help create layered navigation on your app. + +Attribute | Type | Description +--- | --- | --- +`filter_items` | [LayerFilterItemInterface] | An array of filter items +`filter_items_count` | Int | The number of filter items in filter group +`name` | String | The layered navigation filter name +`request_var` | String | The request variable name for the filter query + +### LayerFilterItemInterface + +`LayerFilterItemInterface` contains an array of items that match the terms defined in the filter. + +Attribute | Type | Description +--- | --- | --- +`items_count` | Int | The number of items the filter returned +`label` | String | The label applied to a filter +`value_string` | String | The value for filter request variable to be used in a query + +## Sample queries + +This section illustrates some of the many ways that you can use the `products` query. + +### Full text search + +The following search returns items that contain the word `yoga` or `pants`. The Catalog Search index contains search terms taken from the product `name`, `description`, `short_description` and related attributes. + +**Request:** + +```graphql +{ + products(search: "Yoga pants", pageSize: 2) { + total_count + items { + name + sku + price_range { + minimum_price { + regular_price { + value + currency + } + } + } + } + page_info { + page_size + current_page + } + } +} +``` + +**Response:** + +The search returns 45 items, but only the first two items are returned on the current page. + +```json +{ + "data": { + "products": { + "total_count": 45, + "items": [ + { + "name": "Josie Yoga Jacket", + "sku": "WJ02", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 56.25, + "currency": "USD" + } + } + } + }, + { + "name": "Selene Yoga Hoodie", + "sku": "WH05", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 42, + "currency": "USD" + } + } + } + } + ], + "page_info": { + "page_size": 2, + "current_page": 1 + } + } + } +} +``` + +### Full text search with filter + +The following sample query returns a list of products that meets the following criteria: + +- The product name, product description, or related field contains the string `Messenger` (which causes it to be available for full text searches). +- The SKU begins with `24-MB` +- The price is less than $50. + +The response for each item includes the `name`, `sku`, and `price` only. Up to 25 results are returned at a time, in decreasing order of price. + +**Request:** + +```graphql +{ + products( + search: "Messenger" + filter: { price: { to: "50" } } + pageSize: 25 + sort: { price: DESC } + ) { + items { + name + sku + price_range { + minimum_price { + regular_price { + value + currency + } + } + } + } + total_count + page_info { + page_size + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "products": { + "items": [ + { + "name": "Rival Field Messenger", + "sku": "24-MB06", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 45, + "currency": "USD" + } + } + } + }, + { + "name": "Push It Messenger Bag", + "sku": "24-WB04", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 45, + "currency": "USD" + } + } + } + }, + { + "name": "Wayfarer Messenger Bag", + "sku": "24-MB05", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 45, + "currency": "USD" + } + } + } + } + ], + "total_count": 3, + "page_info": { + "page_size": 25 + } + } + } +} +``` + +### Query with layered navigation + +The following query returns aggregations for a query that filters on items with these characteristics: + +- Women's pants (category ID `27`) +- In the price range of $30 - $39.99 +- Comes in black (color `49`) + +{:.bs-callout-info} +By default, you cannot filter on the `color` attribute. [Filtering with custom attributes]({{page.baseurl}}/graphql/custom-filters.html) describes how to enable this attribute for filtering. You can also run the following query without enabling the attribute by deleting `, color: {eq: "49"}`. + +**Request:** + +```graphql +{ + products(filter: {category_id: {eq: "27"}, price: {from: "30", to: "39.99"}, color: {eq: "49"}}, pageSize: 25, sort: {name: DESC}) { + aggregations { + attribute_code + count + label + options { + label + value + count + } + } + items { + name + sku + price_range { + minimum_price { + regular_price { + value + currency + } + } + } + } + page_info { + page_size + } + } +} +``` + +**Response:** + +{% collapsible Show sample response %} + +```json +{ + "data": { + "products": { + "aggregations": [ + { + "attribute_code": "price", + "count": 1, + "label": "Price", + "options": [ + { + "label": "30-*", + "value": "30_*", + "count": 4 + } + ] + }, + { + "attribute_code": "category_id", + "count": 5, + "label": "Category", + "options": [ + { + "label": "New Luma Yoga Collection", + "value": "8", + "count": 1 + }, + { + "label": "Bottoms", + "value": "22", + "count": 4 + }, + { + "label": "Pants", + "value": "27", + "count": 4 + }, + { + "label": "Pants", + "value": "32", + "count": 4 + }, + { + "label": "Performance Fabrics", + "value": "35", + "count": 2 + } + ] + }, + { + "attribute_code": "color", + "count": 8, + "label": "Color", + "options": [ + { + "label": "Black", + "value": "49", + "count": 4 + }, + { + "label": "Blue", + "value": "50", + "count": 2 + }, + { + "label": "Gray", + "value": "52", + "count": 1 + }, + { + "label": "Green", + "value": "53", + "count": 1 + }, + { + "label": "Orange", + "value": "56", + "count": 1 + }, + { + "label": "Purple", + "value": "57", + "count": 1 + }, + { + "label": "Red", + "value": "58", + "count": 1 + }, + { + "label": "White", + "value": "59", + "count": 1 + } + ] + }, + { + "attribute_code": "material", + "count": 7, + "label": "Material", + "options": [ + { + "label": "Nylon", + "value": "37", + "count": 1 + }, + { + "label": "Rayon", + "value": "39", + "count": 1 + }, + { + "label": "LumaTech™", + "value": "148", + "count": 1 + }, + { + "label": "Microfiber", + "value": "150", + "count": 2 + }, + { + "label": "Spandex", + "value": "151", + "count": 2 + }, + { + "label": "Organic Cotton", + "value": "154", + "count": 2 + }, + { + "label": "CoolTech™", + "value": "156", + "count": 2 + } + ] + }, + { + "attribute_code": "size", + "count": 2, + "label": "Size", + "options": [ + { + "label": "28", + "value": "172", + "count": 4 + }, + { + "label": "29", + "value": "173", + "count": 4 + } + ] + }, + { + "attribute_code": "eco_collection", + "count": 2, + "label": "Eco Collection", + "options": [ + { + "label": "0", + "value": "0", + "count": 3 + }, + { + "label": "1", + "value": "1", + "count": 1 + } + ] + }, + { + "attribute_code": "performance_fabric", + "count": 2, + "label": "Performance Fabric", + "options": [ + { + "label": "0", + "value": "0", + "count": 2 + }, + { + "label": "1", + "value": "1", + "count": 2 + } + ] + }, + { + "attribute_code": "erin_recommends", + "count": 1, + "label": "Erin Recommends", + "options": [ + { + "label": "0", + "value": "0", + "count": 4 + } + ] + }, + { + "attribute_code": "new", + "count": 2, + "label": "New", + "options": [ + { + "label": "0", + "value": "0", + "count": 3 + }, + { + "label": "1", + "value": "1", + "count": 1 + } + ] + }, + { + "attribute_code": "sale", + "count": 1, + "label": "Sale", + "options": [ + { + "label": "0", + "value": "0", + "count": 4 + } + ] + }, + { + "attribute_code": "style_bottom", + "count": 5, + "label": "Style Bottom", + "options": [ + { + "label": "Capri", + "value": "107", + "count": 2 + }, + { + "label": "Leggings", + "value": "109", + "count": 1 + }, + { + "label": "Parachute", + "value": "110", + "count": 1 + }, + { + "label": "Sweatpants", + "value": "113", + "count": 1 + }, + { + "label": "Track Pants", + "value": "115", + "count": 1 + } + ] + }, + { + "attribute_code": "pattern", + "count": 2, + "label": "Pattern", + "options": [ + { + "label": "Color-Blocked", + "value": "195", + "count": 3 + }, + { + "label": "Solid", + "value": "197", + "count": 1 + } + ] + }, + { + "attribute_code": "climate", + "count": 5, + "label": "Climate", + "options": [ + { + "label": "Indoor", + "value": "205", + "count": 4 + }, + { + "label": "Mild", + "value": "206", + "count": 4 + }, + { + "label": "Spring", + "value": "208", + "count": 1 + }, + { + "label": "Warm", + "value": "209", + "count": 2 + }, + { + "label": "Hot", + "value": "212", + "count": 3 + } + ] + } + ], + "items": [ + { + "name": "Karmen Yoga Pant", + "sku": "WP01", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 39, + "currency": "USD" + } + } + } + }, + { + "name": "Ida Workout Parachute Pant", + "sku": "WP03", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 48, + "currency": "USD" + } + } + } + }, + { + "name": "Bardot Capri", + "sku": "WP08", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 48, + "currency": "USD" + } + } + } + }, + { + "name": "Aeon Capri", + "sku": "WP07", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 48, + "currency": "USD" + } + } + } + } + ], + "page_info": { + "page_size": 25 + } + } + } +} +``` + +{% endcollapsible %} + +### Return minimum and maximum prices and discount information + +In the following example, a catalog price rule that provides a 10% discount on all fitness equipment is in effect. The product queried, `24-WG080`, is the Sprite Yoga Companion Kit bundle product. This product has two user-selected options that cause the price to vary. If you choose to query a product that is not a composite (bundle, group, or configurable) product, the minimum and maximum prices are the same. + +**Request:** + +```graphql +{ + products(filter: {sku: {eq: "24-WG080"}}, sort: {name: ASC}) { + items { + name + sku + price_range { + minimum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + maximum_price { + regular_price { + value + currency + } + final_price { + value + currency + } + discount { + amount_off + percent_off + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "products": { + "items": [ + { + "name": "Sprite Yoga Companion Kit", + "sku": "24-WG080", + "price_range": { + "minimum_price": { + "regular_price": { + "value": 61, + "currency": "USD" + }, + "final_price": { + "value": 61, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + }, + "maximum_price": { + "regular_price": { + "value": 77, + "currency": "USD" + }, + "final_price": { + "value": 77, + "currency": "USD" + }, + "discount": { + "amount_off": 0, + "percent_off": 0 + } + } + } + } + ] + } + } +} +``` + +### Sort by a custom attribute + +In this example, the `description` attribute has been enabled by setting the **Stores** > Attributes > **Product** > description > **Storefront Properties** > **Use in Search** and **Used for Sorting in Product Listing** fields to Yes. The query returns all products with a price range of $28 to $30, sorted by the description. + +**Request:** + +```graphql +{ + products(filter: { price: { from: "28" to: "30"} } + sort: { + description: ASC + }) { + total_count + items { + name + sku + description { + html + } + price_range { + maximum_price { + regular_price { + value + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "products": { + "total_count": 25, + "items": [ + { + "name": "Erikssen CoolTech™ Fitness Tank", + "sku": "MT01", + "description": { + "html": "

    A good running tank helps make the miles pass by keep you cool. The Erikssen CoolTech™ Fitness Tank completes that mission, with performance fabric engineered to wick perspiration and promote airflow.

    \n

    • Red performance tank.
    • Slight scoop neckline.
    • Reflectivity.
    • Machine wash/dry.

    " + }, + "price_range": { + "maximum_price": { + "regular_price": { + "value": 29 + } + } + } + }, + { + "name": "Primo Endurance Tank", + "sku": "MT03", + "description": { + "html": "

    Chances are your workout goes beyond free weights, which is why the Primo Endurance Tank employs maximum versatility. Run, lift or play ball – this breathable mesh top will keep you cool during all your activities.

    \n

    • Red heather tank with gray pocket.
    • Chafe-resistant flatlock seams.
    • Relaxed fit.
    • Contrast topstitching.
    • Machine wash/dry.

    " + }, + "price_range": { + "maximum_price": { + "regular_price": { + "value": 29 + } + } + } + }, + ... + } + } +} +``` +### Retrieve related products, up-sells, and cross-sells + +The following query shows how to get related products, up-sells, and cross-sells for a product: + +**Request:** + +```graphql +{ + products(filter: { sku: { eq: "24-WB06" } }) { + items { + id + name + related_products { + id + name + } + upsell_products { + id + name + } + crosssell_products { + id + name + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "products": { + "items": [ + { + "id": 11, + "name": "Endeavor Daytrip Backpack", + "related_products": [], + "upsell_products": [ + { + "id": 1, + "name": "Joust Duffle Bag" + }, + { + "id": 3, + "name": "Crown Summit Backpack" + }, + { + "id": 4, + "name": "Wayfarer Messenger Bag" + }, + { + "id": 5, + "name": "Rival Field Messenger" + }, + { + "id": 6, + "name": "Fusion Backpack" + }, + { + "id": 7, + "name": "Impulse Duffle" + }, + { + "id": 12, + "name": "Driven Backpack" + }, + { + "id": 13, + "name": "Overnight Duffle" + }, + { + "id": 14, + "name": "Push It Messenger Bag" + } + ], + "crosssell_products": [ + { + "id": 18, + "name": "Pursuit Lumaflex™ Tone Band" + }, + { + "id": 21, + "name": "Sprite Foam Yoga Brick" + }, + { + "id": 32, + "name": "Sprite Stasis Ball 75 cm" + }, + { + "id": 45, + "name": "Set of Sprite Yoga Straps" + } + ] + } + ] + } + } +} +``` + +### Media gallery search + +The following query returns media gallery information about the product with the `sku` of `24-MB01`. + +**Request:** + +```graphql +query { + productDetail: products( + pageSize: 5 + filter: { + sku: { eq: "24-MB01" } + } + ) { + total_count + items { + sku + id + name + image { + url + label + } + small_image{ + url + label + } + media_gallery { + url + label + ... on ProductVideo { + video_content { + media_type + video_provider + video_url + video_title + video_description + video_metadata + } + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "productDetail": { + "total_count": 1, + "items": [ + { + "sku": "24-MB01", + "id": 1, + "name": "Joust Duffle Bag", + "image": { + "url": "http://magento2.vagrant130/pub/media/catalog/product/cache/fd3509f20f1e8c87464fb5042a4927e6/m/b/mb01-blue-0.jpg", + "label": "Joust Duffle Bag" + }, + "small_image": { + "url": "http://magento2.vagrant130/pub/media/catalog/product/cache/fd3509f20f1e8c87464fb5042a4927e6/m/b/mb01-blue-0.jpg", + "label": "Joust Duffle Bag" + }, + "media_gallery": [ + { + "url": "http://magento2.vagrant130/pub/media/catalog/product/cache/07660f0f9920886e0f9d3257a9c68f26/m/b/mb01-blue-0.jpg", + "label": "Image" + } + ] + } + ] + } + } +} +``` + +### Query a URL's rewrite information {#urlRewriteExample} + +The following product query returns URL rewrite information about the Joust Duffle Bag. + +**Request:** + +```graphql +{ + products(search: "Joust") { + items { + name + sku + url_rewrites { + url + parameters { + name + value + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "products": { + "items": [ + { + "name": "Joust Duffle Bag", + "sku": "24-MB01", + "url_rewrites": [ + { + "url": "no-route", + "parameters": [ + { + "name": "page_id", + "value": "1" + } + ] + }, + { + "url": "joust-duffle-bag.html", + "parameters": [ + { + "name": "id", + "value": "1" + } + ] + }, + { + "url": "gear/joust-duffle-bag.html", + "parameters": [ + { + "name": "id", + "value": "1" + }, + { + "name": "category", + "value": "3" + } + ] + }, + { + "url": "gear/bags/joust-duffle-bag.html", + "parameters": [ + { + "name": "id", + "value": "1" + }, + { + "name": "category", + "value": "4" + } + ] + } + ] + } + ] + } + } +} +``` diff --git a/src/guides/v2.3/graphql/queries/store-config.md b/src/guides/v2.3/graphql/queries/store-config.md new file mode 100644 index 00000000000..13da40ffe5f --- /dev/null +++ b/src/guides/v2.3/graphql/queries/store-config.md @@ -0,0 +1,308 @@ +--- +group: graphql +title: storeConfig query +redirect_from: + - /guides/v2.3/graphql/reference/store-config.html +--- + +The `storeConfig` query defines information about a store's configuration. You can query a non-default store by [changing the header]({{ page.baseurl }}/graphql/send-request.html) in your GraphQL request. + +## Syntax + +`storeConfig: StoreConfig` + +## Example usage + +### Query a store's configuration + +The following call returns all details of a store's configuration. + +**Request:** + +```graphql +{ + storeConfig { + id + code + website_id + locale + base_currency_code + default_display_currency_code + timezone + weight_unit + base_url + base_link_url + base_static_url + base_media_url + secure_base_url + secure_base_link_url + secure_base_static_url + secure_base_media_url + store_name + } +} +``` + +**Response:** + +```json +{ + "data": { + "storeConfig": { + "id": 1, + "code": "default", + "website_id": 1, + "locale": "en_US", + "base_currency_code": "USD", + "default_display_currency_code": "USD", + "timezone": "America/Chicago", + "weight_unit": "lbs", + "base_url": "http://magento2.vagrant193/", + "base_link_url": "http://magento2.vagrant193/", + "base_static_url": "http://magento2.vagrant193/pub/static/version1536249714/", + "base_media_url": "http://magento2.vagrant193/pub/media/", + "secure_base_url": "http://magento2.vagrant193/", + "secure_base_link_url": "http://magento2.vagrant193/", + "secure_base_static_url": "http://magento2.vagrant193/pub/static/version1536249714/", + "secure_base_media_url": "http://magento2.vagrant193/pub/media/", + "store_name": "My Store" + } + } +} +``` + +### Query a store's theme + +The following query returns information about the store's default title, keywords, and welcome text. + +**Request:** + +```graphql +{ + storeConfig { + default_title + default_keywords + welcome + } +} +``` + +**Response:** + +```json +{ + "data": { + "storeConfig": { + "default_title": "Magento Enterprise Edition", + "default_keywords": "Magento, Varien, E-commerce", + "welcome": "Default welcome msg!" + } + } +} +``` + +### Query a store's CMS configuration + +The following query returns information about the store's content pages. + +**Request:** + +```graphql +{ + storeConfig { + front + cms_home_page + no_route + cms_no_route + cms_no_cookies + show_cms_breadcrumbs + } +} +``` + +**Response:** + +```json +{ + "data": { + "storeConfig": { + "front": "cms", + "cms_home_page": "home", + "no_route": "cms/noroute/index", + "cms_no_route": "no-route", + "cms_no_cookies": "enable-cookies", + "show_cms_breadcrumbs": 1 + } + } +} +``` + +### Query a store's Catalog configuration + +The following query returns information about the store's catalog configuration. + +**Request:** + +```graphql +{ + storeConfig { + product_url_suffix + category_url_suffix + title_separator + list_mode + grid_per_page_values + list_per_page_values + grid_per_page + list_per_page + catalog_default_sort_by + } +} +``` + +**Response:** + +```json +{ + "data": { + "storeConfig": { + "product_url_suffix": ".html", + "category_url_suffix": ".html", + "title_separator": "-", + "list_mode": "grid-list", + "grid_per_page_values": "9,15,30", + "list_per_page_values": "5,10,15,20,25", + "grid_per_page": 9, + "list_per_page": 10, + "catalog_default_sort_by": "position" + } + } +} +``` + +### Query a store's fixed product tax configuration + +The following query returns enumeration values that indicate the store's fixed product tax configuration. + +**Request:** + +```graphql +{ + storeConfig { + category_fixed_product_tax_display_setting + product_fixed_product_tax_display_setting + sales_fixed_product_tax_display_setting + } +} +``` + +**Response:** + +```json +{ + "data": { + "storeConfig": { + "category_fixed_product_tax_display_setting": "EXCLUDE_FPT_WITHOUT_DETAILS", + "product_fixed_product_tax_display_setting": "EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS", + "sales_fixed_product_tax_display_setting": "INCLUDE_FPT_WITHOUT_DETAILS" + } + } +} +``` + +## Output attributes + +### Supported storeConfig attributes + +Use the `storeConfig` attributes to retrieve information about the store's configuration; such as, locale, currency codes, and secure and unsecure URLs. + +Attribute | Data Type | Description | Example +--- | --- | --- | --- +`base_currency_code` | String | The code representing the currency in which Magento processes all payment transactions | `USD` +`base_link_url` | String | A fully-qualified URL that is used to create relative links to the `base_url` | `http://magentohost.example.com/` +`base_static_url` | String | The fully-qualified URL that specifies the location of static view files | `http://magentohost.example.com/pub/static/` +`base_media_url` | String | The fully-qualified URL that specifies the location of user media files | `http://magentohost.example.com/pub/media/` +`base_url` | String | The store's fully-qualified base URL | `http://magentohost.example.com/` +`code` | String | A unique identifier for the store | `default` +`default_display_currency_code` | String | The code representing the currency displayed on the store | `USD` +`id` | Integer | The ID number assigned to the store | `1` +`locale` | String | The store's locale | `en_US` +`secure_base_link_url` | String | A secure fully-qualified URL that is used to create relative links to the `base_url` | `https://magentohost.example.com/` +`secure_base_media_url` | String | The secure fully-qualified URL that specifies the location of user media files | `https://magentohost.example.com/pub/media/` +`secure_base_static_url` | String | The secure fully-qualified URL that specifies the location of static view files | `https://magentohost.example.com/pub/static/` +`secure_base_url` | String | The store's fully-qualified secure base URL | `https://magentohost.example.com/` +`store_name` | String | The store's name | `My Store` +`timezone` | String | The store's time zone | `America/Chicago` +`website_id` | Integer | The ID number assigned to the parent website | `1` +`weight_unit` | String | The weight unit for products | `lbs`, `kgs`, etc + +### Supported theme attributes + +Use the `theme` attributes to retrieve information about the store's thematic elements; such as, footer and header information, copyright text, and logo information. These attributes are defined in the `ThemeGraphQl` module. + +Attribute | Data Type | Description +--- | --- | --- +`absolute_footer` | String | Contains scripts that must be included in the HTML before the closing `` tag +`copyright` | String | The copyright statement that appears at the bottom of each page +`default_description` | String | The description that provides a summary of your site for search engine listings and should not be more than 160 characters in length +`default_keywords` | String | A series of keywords that describe your store, each separated by a comma +`default_title` | String | The title that appears at the title bar of each page when viewed in a browser +`demonotice` | Int | Controls the display of the demo store notice at the top of the page. Options: `0` (No) or `1` (Yes) +`head_includes` | String | Contains scripts that must be included in the HTML before the closing `` tag +`head_shortcut_icon` | String | Uploads the small graphic image that appears in the address bar and tab of the browser +`header_logo_src` | String | The path to the logo that appears in the header +`logo_alt` | String | The Alt text that is associated with the logo +`logo_height` | Int | The height of your logo image in pixels +`logo_width` | Int | The width of your logo image in pixels +`title_prefix` | String | A prefix that appears before the title to create a two- or three-part title +`title_suffix` | String | A suffix that appears after the title to create a two-or three part title +`welcome` | String | Text that appears in the header of the page and includes the name of customers who are logged in + +### Supported CMS attributes + +Use the `cms` attributes to retrieve information about the store's default pages. These attributes are defined in the `CmsGraphQl` module. + +Attribute | Data Type | Description +--- | --- | --- +`cms_home_page` | String | Returns the name of the CMS page that identifies the home page for the store +`cms_no_cookies` | String | Identifies a specific CMS page that appears when cookies are not enabled for the browser +`cms_no_route` | String | Identifies a specific CMS page that you want to appear when a 404 “Page Not Found” error occurs +`front` | String | Indicates the landing page that is associated with the base URL +`no_route` | String | Contains the URL of the default page that you want to appear when if a 404 “Page not Found” error occurs +`show_cms_breadcrumbs` | Int | Determines if a breadcrumb trail appears on all CMS pages in the catalog. Options: `0` (No) or `1` (Yes) + +### Supported Catalog attributes + +Use the `catalog` attributes to retrieve information about the store's catalog. These attributes are defined in the `CatalogGraphQl` module. + +Attribute | Data Type | Description | Example +--- | --- | --- +`catalog_default_sort_by` | String | The default sort order of the search results list | `position` +`category_url_suffix` | String | The suffix applied to category pages, such as `.htm` or `.html` | `.html` +`grid_per_page` | Int | The default number of products per page in Grid View | `9` +`grid_per_page_values` | A list of numbers that define how many products can be displayed in List View | `9,15,30` +`list_mode` | String | The format of the search results list | `grid-list` +`list_per_page` | Int | The default number of products per page in List View | `10` +`list_per_page_values` | String | A list of numbers that define how many products can be displayed in List View | `5,10,15,20,25` +`product_url_suffix` | String | The suffix applied to product pages, such as `.htm` or `.html` | `.html` +`root_category_id` | Int | The ID of the root category +`title_separator` | String | Identifies the character that separates the category name and subcategory in the browser title bar | `-` + +### Supported WEEE (fixed product tax) attributes + +The **Stores** > Settings > **Configuration** > **Sales** > **Tax** > **Fixed Product Taxes** panel contains several fields that determine how to display fixed product tax (FPT) values and descriptions. Use the following attributes to determine the values of the **Fixed Product Taxes** fields. These attributes are defined in the `WeeeGraphQl` module. + +Attribute | Data Type | Description +--- | --- | --- +`category_fixed_product_tax_display_setting` | FixedProductTaxDisplaySettings | Corresponds to the **Display Prices In Product Lists** field. It indicates how FPT information is displayed on category pages +`product_fixed_product_tax_display_setting` | FixedProductTaxDisplaySettings | Corresponds to the **Display Prices On Product View Page** field. It indicates how FPT information is displayed on product pages +`sales_fixed_product_tax_display_setting` | FixedProductTaxDisplaySettings | Corresponds to the **Display Prices In Sales Modules** field. It indicates how FPT information is displayed on cart, checkout, and order pages + +The `FixedProductTaxDisplaySettings` data type is an enumeration that describes whether displayed prices include fixed product taxes and whether Magento separately displays detailed information about the FPTs. + +Value | Description +--- | --- +EXCLUDE_FPT_AND_INCLUDE_WITH_DETAILS | The displayed price does not include the FPT amount. You must display the values of `ProductPrice.fixed_product_taxes` and the price including the FPT separately. This value corresponds to **Excluding FPT, Including FPT description and final price** +EXCLUDE_FPT_WITHOUT_DETAILS | The displayed price does not include the FPT amount. The values from `ProductPrice.fixed_product_taxes` are not displayed. This value corresponds to **Excluding FPT** +FPT_DISABLED | The FPT feature is not enabled. You can omit `ProductPrice.fixed_product_taxes` from your query +INCLUDE_FPT_WITH_DETAILS | The displayed price includes the FPT amount while displaying the values of `ProductPrice.fixed_product_taxes` separately. This value corresponds to **Including FPT and FPT description** +INCLUDE_FPT_WITHOUT_DETAILS | The displayed price includes the FPT amount without displaying the `ProductPrice.fixed_product_taxes` values. This value corresponds to **Including FPT only** diff --git a/src/guides/v2.3/graphql/queries/url-resolver.md b/src/guides/v2.3/graphql/queries/url-resolver.md new file mode 100644 index 00000000000..8cfe6b7cc98 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/url-resolver.md @@ -0,0 +1,142 @@ +--- +group: graphql +title: urlResolver query +redirect_from: + - /guides/v2.3/graphql/reference/url-resolver.html +--- + +A merchant can reconfigure (rewrite) the URL to any product, category, or CMS page. When the rewrite goes into effect, any links that point to the previous URL are redirected to the new address. + +The `urlResolver` query returns the canonical URL for a specified product, category, or CMS page. An external app can render a page by a URL without any prior knowledge about the landing page. + +## Syntax + +`{urlResolver(url: String!): EntityUrl}` + +## Example usage + +### URL resolver for a product + +The following query returns information about the URL containing `joust-duffle-bag.html` for a product. + +**Request:** + +```graphql +{ + urlResolver(url: "joust-duffle-bag.html") { + id + relative_url + redirectCode + type + } +} +``` + +**Response:** + +```json +{ + "data": { + "urlResolver": { + "id": 1, + "relative_url": "catalog/product/view/id/1", + "redirectCode": 0, + "type": "PRODUCT" + } + } +} +``` + +### URL resolver for a category + +The following query returns information about the URL containing `gear/bags.html` for category page. + +**Request:** + +```graphql +{ + urlResolver(url: "gear/bags.html") { + id + relative_url + redirectCode + type + } +} +``` + +**Response:** + +```json +{ + "data": { + "urlResolver": { + "id": 4, + "relative_url": "gear/bags.html", + "redirectCode": 0, + "type": "CATEGORY" + } + } +} +``` + +### URL resolver for a CMS page + +The following query returns information about the URL containing `no-route` for a CMS page. + +**Request:** + +```graphql +{ + urlResolver(url: "no-route") { + id + relative_url + redirectCode + type + } +} +``` + +**Response:** + +```json +{ + "data": { + "urlResolver": { + "id": 1, + "relative_url": "no-route", + "redirectCode": 0, + "type": "CMS_PAGE" + } + } +} +``` + +## Input attributes + +The `urlResolver` query contains the following attribute. + +Attribute | Type | Description +--- | --- | --- +`url` | String | The requested URL. To query for product and category pages, the `url` value must contain the URL key and suffix. For category page queries, the `url` value must contain the full path of the URL key. For CMS page queries, the `url` value must contain the URL key only. + +## Output attributes + +The `EntityUrl` output object contains the `id`, `relative_url`, and `type` attributes. + +Attribute | Data Type | Description +--- | --- | --- +`canonical_url` | String | Deprecated. Use `relative_url` instead +`id` | Int | The ID assigned to the object associated with the specified `url`. This could be a product ID, category ID, or page ID +`redirectCode` | Int | Contains 0 when there is no redirect error. A value of 301 indicates the URL of the requested resource has been changed permanently, while a value of 302 indicates a temporary redirect +`relative_url` | String | The internal relative URL. If the specified `url` is a redirect, the query returns the redirected URL, not the original +`type` | UrlRewriteEntityTypeEnum | The value of `UrlRewriteEntityTypeEnum` is one of PRODUCT, CATEGORY, or CMS_PAGE + +## Related topics + +[Products endpoint]({{page.baseurl}}/graphql/queries/products.html) + +## Errors + +Error | Description +--- | --- +`Field urlResolver.url of type String! is required but not provided.` | The value specified in the `urlResolver.url` argument is empty. \ No newline at end of file diff --git a/src/guides/v2.3/graphql/queries/wishlist.md b/src/guides/v2.3/graphql/queries/wishlist.md new file mode 100644 index 00000000000..77fa9128b12 --- /dev/null +++ b/src/guides/v2.3/graphql/queries/wishlist.md @@ -0,0 +1,99 @@ +--- +group: graphql +title: wishlist query +redirect_from: + - /guides/v2.3/graphql/reference/wishlist.html +--- + +{:.bs-callout-warning} +The `wishlist` query has been deprecated. Wish list information is now provided by the [customer]({{page.baseurl}}/graphql/queries/customer.html) query. + +Use the `wishlist` query to retrieve information about a customer's wish list. [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) describes how to supply an authorization token for a specific customer. + +## Syntax + +`wishlist: WishlistOutput` + +## Example usage + +The following query returns the customer's wish list: + +**Request:** + +```graphql +{ + wishlist { + items_count + name + sharing_code + updated_at + items { + id + qty + description + added_at + product { + sku + name + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "wishlist": { + "items_count": 2, + "name": "Wish List", + "sharing_code": "KAXDj0HlM7Y2s58mllsVhSJvRj4fWIZj", + "updated_at": "2019-02-13 22:47:45", + "items": [ + { + "id": 1, + "qty": 1, + "description": "My first priority", + "added_at": "2019-02-20 14:38:02", + "product": { + "sku": "MJ09", + "name": "Taurus Elements Shell" + } + }, + { + "id": 2, + "qty": 1, + "description": null, + "added_at": "2019-02-20 14:38:28", + "product": { + "sku": "MSH11", + "name": "Arcadio Gym Short" + } + } + ] + } + } +} +``` + +## Output attributes + +Attribute | Data type | Description +--- | --- | --- +`items` | [WishlistItem](#wishlistitem) | An array of items in the customer's wish list +`items_count` | Int | The number of items in the wish list +`name` | String | When multiple wish lists are enabled, the name the customer assigns to the wish list +`sharing_code` | String | An encrypted code that Magento uses to link to the wish list +`updated_at` | String | The time of the last modification to the wish list + +### Wish list item attributes {#wishlistitem} + +Attribute | Data type | Description +--- | --- | --- +`added_at` | String | The time when the customer added the item to the wish list +`description` | String | The customer's comment about this item +`id` | Int | The wish list item ID +`product` | [ProductInterface]({{ page.baseurl }}/graphql/interfaces/product-interface.html) | The ProductInterface contains attributes that are common to all types of products. Note that descriptions may not be available for custom and EAV attributes +`qty` | Float | The quantity of this wish list item diff --git a/src/guides/v2.3/graphql/release-notes.md b/src/guides/v2.3/graphql/release-notes.md new file mode 100644 index 00000000000..734260d2aa6 --- /dev/null +++ b/src/guides/v2.3/graphql/release-notes.md @@ -0,0 +1,129 @@ +--- +group: graphql +title: Release Notes +--- + +*Release notes published January 2020.* + +GraphQL is a flexible and performant API that allows you to build custom front-ends, including headless storefronts, [Progressive Web Apps](https://developer.adobe.com/commerce/pwa-studio/) (PWA), and mobile apps for Magento. + +The **[Magento GraphQL](https://github.com/magento/graphql-ce) project** is a Magento Community Engineering special project open to contributors. +To take part and contribute, see the [Magento GraphQL](https://github.com/magento/graphql-ce) repository and [wiki](https://github.com/magento/graphql-ce/wiki) to get started. Join us in our [Slack](https://magentocommeng.slack.com/archives/C8076E0KS) channel (or [self signup](https://opensource.magento.com/slack)) to discuss the project. + +These release notes can include: + +- {:.new}New features +- {:.fix}Fixes and improvements + +## {{site.data.var.ee}} and {{site.data.var.ce}} 2.3.5 + +- {:.new} **The `products` and `categoryList` queries can now be used to retrieve information about products and categories that have been added to a staged campaign.** These queries require an admin authorization token. See [Using queries]({{ page.baseurl }}/graphql/queries/index.html#staging) for details. +- {:.fix} Custom attributes used in layered navigation no longer require the **Use in Search**, **Visible in Advanced Search**, and **Use in Search Results Layered Navigation** fields be set to Yes. +- {:.fix} Added the `position` and `disabled` attributes to the `MediaGalleryInterface`. +- {:.fix} When you apply a gift card to a cart, an exception is no longer thrown when the last product is removed from the cart. +- {:.fix} In a category search, the `image` attribute returns the full path to an image, rather than a truncated path. +- {:.fix} Flat rate shipping amounts are calculated correctly when you add a configurable product to a cart. + +## {{site.data.var.ee}} and {{site.data.var.ce}} 2.3.4 + +- {:.new} **Guest carts can now be merged with customer carts.** The [`mergeCarts`]({{page.baseurl}}/graphql/mutations/merge-carts.html) mutation transfers the contents of a guest cart into the cart of a logged-in customer. +- {:.new} **A customer can start an order on one device and complete it on another.** Use the [`customerCart`]({{page.baseurl}}/graphql/queries/customer-cart.html) query to obtain the cart ID for a logged-in customer. +- {:.new} **Layered navigation can use custom filters.** The `filter` attribute of the [`products`]({{page.baseurl}}/graphql/queries/products.html) query now requires the `ProductAttributeFilterInput` object. You can specify a pre-defined filter in this object, or [define a custom filter]({{page.baseurl}}/graphql/custom-filters.html). As a result, layered navigation on your website filters on the attributes you need. +- {:.new} **You can search categories by ID, name, and/or URL key.** The [`categoryList`]({{page.baseurl}}/graphql/queries/category-list.html) query replaces the deprecated `category` query. +- {:.new} **A customer can add bundle and downloadable products to the cart with the [`addBundleProductsToCart`]({{page.baseurl}}/graphql/mutations/add-bundle-products.html) and [`addDownloadableProductsToCart`]({{page.baseurl}}/graphql/mutations/add-downloadable-products.html) mutations.** +- {:.new} **The [`ProductInterface`]({{page.baseurl}}/graphql/interfaces/product-interface.html) supports fixed product taxes (such as WEEE).** Use the [`storeConfig`]({{page.baseurl}}/graphql/queries/store-config.html) query to determine whether the store supports these taxes. +- {:.new} **The [`cart`]({{page.baseurl}}/graphql/queries/cart.html) object has been enhanced to include information about promotions and applied discounts at the line and cart levels.** +- {:.new} **Added test coverage in multiple GraphQL modules.** + +The following queries and mutations have been deprecated: + +Deprecated entity | Use this instead +--- | --- +`category` query | `categoryList` query +`setPaymentMethodOnCartAndPlaceOrder` mutation | Run the `setPaymentMethodOnCart` and `placeOrder` mutations in the same request +`wishlist` query | `customer` query + +## {{site.data.var.ee}} and {{site.data.var.ce}} 2.3.3 + +- {:.new} **GraphQL supports PayPal, Braintree, and Authorize.Net payment methods.** You can use mutations to set the payment method, retrieve payment method-specific tokens, and place an order. For details, see the following topics: + + - [Authorize.Net]({{page.baseurl}}/graphql/payment-methods/authorize-net.html) + - [Braintree]({{page.baseurl}}/graphql/payment-methods/braintree.html) + - [Braintree Vault]({{page.baseurl}}/graphql/payment-methods/braintree-vault.html) + - [PayPal Express Checkout]({{page.baseurl}}/graphql/payment-methods/paypal-express-checkout.html) + - [PayPal Payflow Link]({{page.baseurl}}/graphql/payment-methods/payflow-link.html) + - [PayPal Payflow Pro]({{page.baseurl}}/graphql/payment-methods/payflow-pro.html) + - [PayPal Payments Advanced]({{page.baseurl}}/graphql/payment-methods/payments-advanced.html) + - [PayPal Website Payments Pro Hosted Solution]({{page.baseurl}}/graphql/payment-methods/hosted-pro.html) + - [Express Checkout for other PayPal solutions]({{page.baseurl}}/graphql/payment-methods/payflow-express.html) + +- {:.new} **Added support for gift cards:** ({{site.data.var.ee}} only) + - [`giftCardAccount`]({{page.baseurl}}/graphql/queries/giftcard-account.html) query + - [`applyGiftCardToAccount`]({{page.baseurl}}/graphql/mutations/apply-giftcard.html) mutation + - [`removeGiftCardFromCart`]({{page.baseurl}}/graphql/mutations/remove-giftcard.html) mutation + +- {:.new} **Added the ability to manage store credit:** ({{site.data.var.ee}} only) + - [`applyStoreCreditToCart`]({{page.baseurl}}/graphql/mutations/apply-store-credit.html) mutation + - [`removeStoreCreditFromCart`]({{page.baseurl}}/graphql/mutations/remove-store-credit.html) mutation + +- {:.new} **Added the [addConfigurableProductsToCart]({{page.baseurl}}/graphql/mutations/add-configurable-products.html) mutation.** + +## {{site.data.var.ce}} 2.3.2 + +- {:.new} **Added mutations to support the following cart operations and checkout for logged-in and guest customers:** + + - Add [simple products]({{page.baseurl}}/graphql/mutations/add-simple-products.html) to a cart. + - Add [virtual products]({{page.baseurl}}/graphql/mutations/add-virtual-products.html) to a cart. + - Set the [shipping address]({{page.baseurl}}/graphql/mutations/set-shipping-address.html). Address books are supported. + - Set the [billing address]({{page.baseurl}}/graphql/mutations/set-billing-address.html). Address books are supported. + - Set the [shipping method]({{page.baseurl}}/graphql/mutations/set-shipping-method.html). Supported methods include DHL, FedEx, Flat Rate, Free Shipping, Table Rate, UPS, and USPS. + - Set the [payment method]({{page.baseurl}}/graphql/mutations/set-payment-method.html). Supported methods include Bank Transfer, Cash on Delivery, Check/Money Order, Purchase Order, and Zero Subtotal Checkout. + - [Apply]({{page.baseurl}}/graphql/mutations/apply-coupon.html) or [remove]({{page.baseurl}}/graphql/mutations/remove-coupon.html) cart coupons. + - [Assign an email]({{page.baseurl}}/graphql/mutations/set-guest-email.html) to a guest cart. + - [Place an order]({{page.baseurl}}/graphql/mutations/place-order.html). + +- {:.new} **Added support for payment methods that implement Magento Vault. See [customerPaymentTokens query]({{page.baseurl}}/graphql/queries/customer-payment-tokens.html) and [deletePaymentToken mutation]({{page.baseurl}}/graphql/mutations/delete-payment-token.html)** + +- {:.new} **Added new queries and extended the functionality of others.** + + - The [`isEmailAvailable` query]({{page.baseurl}}/graphql/queries/is-email-available.html) checks whether the specified email address has already been used to create an account. + - The [`cart` query]({{page.baseurl}}/graphql/queries/cart.html) can now return information set by mutations that perform cart operations, including product information, shipping and billing addresses, shipping and payment methods, and applied coupons. The query also returns calculated totals. + - The `customerPaymentTokens` query returns the signed-in customer's payment tokens. + +- {:.new} **Queries can now be performed as HTTP GET or POST operations.** + +- {:.new} **Magento can use Varnish or full-page caching to [cache]({{page.baseurl}}/graphql/caching.html) pages rendered from the results of the following GraphQL queries:** + + - `category` + - `cmsBlocks` + - `cmsPage` + - `products` + - `urlResolver` + + You must send these queries as HTTP GET operations to cache the results. + +## {{site.data.var.ce}} 2.3.1 + +- {:.new} **Added mutations and queries that allow customers to manage My Account information.** Specific capabilities include: + - Create [customer]({{page.baseurl}}/graphql/mutations/create-customer.html) account + - Change account information + - Manage billing and shipping addresses + - Change customer password + - Manage newsletter subscriptions + - View [wish lists]({{page.baseurl}}/graphql/queries/wishlist.html) + - View [order history]({{page.baseurl}}/graphql/queries/customer-orders.html) + - View [downloadable products]({{page.baseurl}}/graphql/interfaces/downloadable-product.html) + +- {:.new} **Added functionality to support complex Catalog features.** This version supports: + - Specifying absolute image paths for [products]({{page.baseurl}}/graphql/queries/products.html) and including extended image information + - Rendering fields that use WYSIWYG text + - URL rewrites for products​ + +- {:.new} **GraphQL framework enhancements**, including: + - Mutations that [generate]({{page.baseurl}}/graphql/mutations/generate-customer-token.html) and [revoke]({{page.baseurl}}/graphql/mutations/revoke-customer-token.html) customer tokens + - Page Builder and WYSIWYG fields support complex structures for PWA scenarios + - Magento now calculates the complexity of queries and mutations and returns an error message if a query or mutation is deemed too complex + - Variable support in [queries]({{page.baseurl}}/graphql/queries/index.html) and [mutations]({{page.baseurl}}/graphql/mutations/index.html) + - A query that returns information about a store's [theme and CMS]({{page.baseurl}}/graphql/queries/store-config.html) configuration + - GraphQL tests are integrated with Travis CI​ + - GraphQL browsers now display fields and objects alphabetically diff --git a/src/guides/v2.3/graphql/security-configuration.md b/src/guides/v2.3/graphql/security-configuration.md new file mode 100644 index 00000000000..115ffda055f --- /dev/null +++ b/src/guides/v2.3/graphql/security-configuration.md @@ -0,0 +1,129 @@ +--- +group: graphql +title: GraphQL security configuration +--- + +The Framework and `GraphQl` module `di.xml` files define several security-related configuration values that you should review to ensure they align with types of mutations and queries that you run. + +To override these default values, create a custom module and provide a new value in the appropriate [di.xml]({{page.baseurl}}/extension-dev-guide/build/di-xml-file.html) file. + +## Input limiting + +In GraphQL, you can limit the maximum page size allowed. For information about how to enable and configure this feature, as well as additional arguments that are applicable to web APIs in general, see [API security]({{page.baseurl}}/get-started/api-security.html). + +## GraphQl module configuration + +The `GraphQl/etc/di.xml` file contains two arguments that can be overridden to enhance security and prevent performance bottlenecks: + +Attribute | Default value | Description +--- | --- | --- +`queryComplexity` | 300 | Defines the maximum number of fields, objects, and fragments that a query can contain. +`queryDepth` | 20 | Defines the maximum depth of nodes that query can return. + +### Query complexity + +A complex GraphQL query, such as the [`cart`]({{page.baseurl}}/graphql/queries/cart.html) or [`products`]({{page.baseurl}}/graphql/queries/products.html) query, can potentially generate a heavy workload on the server. Complex queries can potentially be used to create distributed denial of service (DDoS) attacks by overloading the server with specious requests. + +Each instance of the following items adds 1 to the complexity score: + +* A field and parent field in the body of the query. +* A field in an inline fragment. +* A field in a fragment spread. If a fragment spread is used multiple times, each field within is counted that number of times. + +The following items do not count toward the complexity score: + +* The root `query` field +* Fragment declarations +* Fragment spread declarations + +The following sample query contains all of the items listed above. + +```graphql +query { + countries { + full_name_english + name1: full_name_english + ...on Country { + two_letter_abbreviation + } + ...myFrag + ...myFrag + } +} +fragment myFrag on Country { + three_letter_abbreviation +} +``` + +The complexity count for the query is 6. These lines contributed to the count: + +* `countries {}` +* `full_name_english` (first instance) +* `name1: full_name_english` +* `two_letter_abbreviation` +* `three_letter_abbreviation` (first instance of `...myFrag`) +* `three_letter_abbreviation` (second instance of `...myFrag`) + +Creating the `name1` alias did not cause the system to double count the entry. + +If the count does not exceed the threshold set by the `queryComplexity` attribute, Magento validates and processes the query. + +### Query depth + +The `queryDepth` attribute specifies the maximum depth a query can return. This can be an issue for queries that return objects that show a hierarchy, such as [`CategoryTree`]({{page.baseurl}}/graphql/queries/category-list.html), or queries that return detailed data on complex [products]({{page.baseurl}}/graphql/queries/products.html). The default value of 20 allows for deep hierarchies and products, but you might want to reduce this number if you know that legitimate queries will never reach that depth. + +The following query has a maximum depth of 5. + +```graphql +{ + categories( + filters: { + parent_id: {in: ["2"]} + } + ) { + total_count + items { + uid + level + name + path + children_count + children { + uid + level + name + path + children_count + children { + uid + level + name + path + children_count + children { + uid + level + name + path + } + } + } + } + page_info { + current_page + page_size + total_pages + } + } +} +``` + +These fields contribute to the depth: + +* `items` +* `children` (first instance) +* `children` (second instance) +* `children` (third instance) +* `uid` and other fields in this node + +If the depth of the query exceeds the value `queryDepth`, the system returns an error. diff --git a/src/guides/v2.3/graphql/send-request.md b/src/guides/v2.3/graphql/send-request.md new file mode 100644 index 00000000000..13716c73592 --- /dev/null +++ b/src/guides/v2.3/graphql/send-request.md @@ -0,0 +1,71 @@ +--- +group: graphql +title: GraphQL requests +--- + +Magento GraphQL supports the HTTP GET and POST methods. You can send a query as a GET or POST request. Mutations must be POST requests. You can optionally send a GET query request in a URL. In these requests, you must specify `query` as the query string. You might need to encode the query, as shown below: + +`http:///graphql?query=%7Bproducts(filter%3A%7Bsku%3A%7Beq%3A%2224-WB01%22%7D%7D)%7Bitems%7Bname%20sku%7D%7D%7D` + +The previous example is equivalent to the following query. You could send the query as either a GET or POST request. + +**Request:** + +```graphql +{ + products( + filter: { sku: { eq: "24-WB01" } } + ) { + items { + name + sku + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "products": { + "items": [ + { + "name": "Voyage Yoga Bag", + "sku": "24-WB01" + } + ] + } + } +} +``` + +Some queries sent as a GET request can be cached. See [GraphQL caching]({{page.baseurl}}/graphql/caching.html) for more information. + +## Request headers {#headers} + +Magento accepts the following headers in a GraphQL request: + +Header name | Value | Description +--- | --- | --- +`Authorization` | `Bearer ` | A customer or admin token. [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) describes how to generate tokens. +`Content-Currency` | A valid currency code, such as `USD` | This header is required only if the currency is not the store view's default currency. +`Content-Type` | `application/json` | Required for all requests. +`Preview-Version` | A timestamp (seconds since January 1, 1970). | Use this header to query products, categories, price rules, and other entities that are scheduled to be in a campaign (staged content). Staging is supported in {{site.data.var.ee}} only. +`Store` | `` | The store view code on which to perform the request. The value can be `default` or the code that is defined when a store view is created. +`X-Captcha` | Shopper-entered CAPTCHA value | Required when a shopper enters a CAPTCHA value on the frontend, unless an integration token is provided. Forms requiring CAPTCHA values are configured at **Stores** > **Configuration** > **Customers** > **Customer Configuration** > **CAPTCHA** > **Forms**. + +### Specify request headers in a GraphQL browser + +GraphQL browsers, such as GraphiQL, allow you to enter a set of header name/value pairs. The following example shows an example customer authorization token and content type headers. + +![GraphiQL Authorization Bearer]({{site.baseurl}}/common/images/graphql/graphql-authorization.png) + +### Specify request headers with the `curl` command + +Use the curl command with a separate `-H` argument to specify each request header. The following example uses the same request headers as those used in the GraphQL browser. + +```bash +curl 'http://magento.config/graphql' -H 'Authorization: Bearer hoyz7k697ubv5hcpq92yrtx39i7x10um' -H 'Content-Type: application/json' --data-binary '{"query":"query {\n customer {\n firstname\n lastname\n email\n }\n}"}' +``` diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-add-product-to-cart.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-add-product-to-cart.md new file mode 100644 index 00000000000..278eb16872a --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-add-product-to-cart.md @@ -0,0 +1,167 @@ +--- +layout: tutorial +group: graphql +title: Step 3. Add products to the cart +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 30 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +GraphQL supports all product types, but this tutorial only demonstrates how to add a simple product and a virtual product to the shopping cart. You can find more details and examples in the following topics: + +- [Bundle products]({{ page.baseurl }}/graphql/mutations/add-bundle-products.html) +- [Configurable products]({{ page.baseurl }}/graphql/mutations/add-configurable-products.html) +- [Downloadable products]({{ page.baseurl }}/graphql/mutations/add-downloadable-products.html) +- [Simple and grouped products]({{ page.baseurl }}/graphql/mutations/add-simple-products.html) +- [Virtual products]({{ page.baseurl }}/graphql/mutations/add-virtual-products.html) + +[Product interface implementations]({{ page.baseurl }}/graphql/interfaces/product-interface-implementations.html) also describes how to create queries that access product interfaces. + +Use the `updateCartItems` mutation to update shopping cart items and `removeItemFromCart` to remove a product from the shopping cart. + +{:.bs-callout-info} +If you add a product to the shopping cart as a registered customer, be sure to send the customer's authorization token in the `Authorization` parameter of the header. See [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) for more details. + +## Add a simple product into the shopping cart + +The following mutation adds a simple product into the shopping cart. + +Replace `{ CART_ID }` with the unique shopping cart ID from [Step 2. Create empty cart]({{ page.baseurl }}/graphql/tutorials/checkout/checkout-add-product-to-cart.html). + +In this example, we will add the Aim Analog Watch (SKU 24-MG04) from the default Luma catalog to the cart. The SKU identifies the product to be added. + +**Request:** + +```graphql +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{ CART_ID }" + cart_items: [ + { + data: { + quantity: 1 + sku: "24-MG04" + } + } + ] + } + ) { + cart { + items { + id + product { + sku + stock_status + } + quantity + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addSimpleProductsToCart": { + "cart": { + "items": [ + { + "id": "5", + "product": { + "sku": "24-MG04", + "stock_status": "IN_STOCK" + }, + "quantity": 1 + } + ] + } + } + } +} +``` + +## Add a virtual product into the shopping cart + +The following mutation adds a virtual product into the shopping cart. +In this example, we add the Beginner's Yoga video downloadable product (SKU 240-LV04). + +**Request:** + +```graphql +mutation { + addVirtualProductsToCart( + input: { + cart_id: "{ CART_ID }" + cart_items: [ + { + data: { + quantity: 1 + sku: "240-LV04" + } + } + ] + } + ) { + cart { + items { + id + product { + sku + stock_status + } + quantity + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "addVirtualProductsToCart": { + "cart": { + "items": [ + { + "id": "5", + "product": { + "sku": "24-MG04", + "stock_status": "IN_STOCK" + }, + "quantity": 1 + }, + { + "id": "6", + "product": { + "sku": "240-LV04", + "stock_status": "IN_STOCK" + }, + "quantity": 1 + } + ] + } + } + } +} +``` + +The response lists all items currently in the cart, including the just-added video download. + +## Verify this step {#verify-step} + +1. Sign in as a customer to the website using the email `john.doe@example.com` and password `b1b2b3l@w+`. + +1. Go to the shopping cart. All the items you added are displayed. diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-billing-address.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-billing-address.md new file mode 100644 index 00000000000..57dbc8e4746 --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-billing-address.md @@ -0,0 +1,114 @@ +--- +layout: tutorial +group: graphql +title: Step 5. Set billing address +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 50 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +{:.bs-callout-tip} +You must always set the billing address to place an order. + +Use the [setBillingAddressOnCart]({{ page.baseurl }}/graphql/mutations/set-billing-address.html) mutation to set a billing address. + +## Add a billing address to the cart + +Similar to the shipping address, add a billing address to the cart. `{ CART_ID }` is the unique shopping cart ID from [Step 2. Create empty cart]({{ page.baseurl }}/graphql/tutorials/checkout/checkout-add-product-to-cart.html). The street address is also different, so we can see that different addresses are being created. + +Send the customer's authorization token in the `Authorization` parameter of the header. See [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) for more information. + +**Request:** + +```graphql +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{ CART_ID }" + billing_address: { + address: { + firstname: "John" + lastname: "Doe" + company: "Company Name" + street: ["64 Strawberry Dr", "Beverly Hills"] + city: "Los Angeles" + region: "CA" + region_id: 12 + postcode: "90210" + country_code: "US" + telephone: "123-456-0000" + save_in_address_book: true + } + } + } + ) { + cart { + billing_address { + firstname + lastname + company + street + city + region{ + code + label + } + postcode + telephone + country { + code + label + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setBillingAddressOnCart": { + "cart": { + "billing_address": { + "firstname": "John", + "lastname": "Doe", + "company": "Company Name", + "street": [ + "64 Strawberry Dr", + "Beverly Hills" + ], + "city": "Los Angeles", + "region": { + "code": "CA", + "label": "California" + }, + "postcode": "90210", + "telephone": "123-456-0000", + "country": { + "code": "US", + "label": "US" + } + } + } + } + } +} +``` + +## Verify this step {#verify-step} + +1. Sign in as a customer to the website using the email `john.doe@example.com` and password `b1b2b3l@w+`. + +1. Go to Checkout. + +1. Go to the Review & Payments step. The Billing Address form is populated with the address details you entered. diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-coupon.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-coupon.md new file mode 100644 index 00000000000..293c10e1bf4 --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-coupon.md @@ -0,0 +1,133 @@ +--- +layout: tutorial +group: graphql +title: Step 7. Apply a coupon +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 70 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +Use [applyCouponToCart]({{ page.baseurl }}/graphql/mutations/apply-coupon.html) to apply a discount coupon to the specified `cart_id`. + +`{ CART_ID }` is the unique shopping cart ID from [Step 2. Create empty cart]({{ page.baseurl }}/graphql/tutorials/checkout/checkout-add-product-to-cart.html). + +`{ COUPON_CODE }` is an existing Magento coupon code. It cannot be generated with GraphQL. + +## Create a coupon + +Coupons must be generated from the Admin. + +Creating a coupon is described in [Coupon Codes]({{ site.user_guide_url }}/marketing/price-rules-cart-coupon.html). +For the purpose of this tutorial, create a Cart Price Rule with: + +For **Rule Information**: + +- **Rule Name**: Watch Coupon +- **Active**: Yes +- **Websites**: Main Website +- **Customer Groups**: Select all of them +- **Coupon**: Specific Coupon +- **Coupon Code**: Watch20 +- **Uses per Coupon**: 5 +- **Uses per Customer**: 5 + +For **Actions** + +- **Apply**: Percent of product price discount +- **Discount Amount**: 20 + +Save this rule. +The **Coupon Code** value is the name of the coupon the end user enters. +To verify the coupon works, create an order with a product using guest checkout. +When checking out, enter `Watch20` in the Apply Discount Code field and press the Apply Discount button. +The discount should be applied in the cart. + +When the coupon is set up, we can apply it via GraphQL. Replace the `{ CART_ID }` with your generated ID and replace the `{ COUPON_CODE }` with `Watch20` below. + +For logged-in customers, send the customer's authorization token in the `Authorization` parameter of the header. See [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) for more information. + +**Request:** + +```graphql +mutation { + applyCouponToCart( + input: { + cart_id: "{ CART_ID }" + coupon_code: "{ COUPON_CODE }" + } + ) { + cart { + applied_coupons { + code + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "applyCouponToCart": { + "cart": { + "applied_coupons": [ + { + "code": "Watch20" + } + ] + } + } + } +} +``` + +## Verify this step {#verify-step} + +1. Sign in as a customer to the website using the email `john.doe@example.com` and password `b1b2b3l@w+`. + +1. Go to Checkout. + +1. The discount is displayed in the Order Summary block. + +## Remove a coupon + +Use [removeCouponFromCart]({{ page.baseurl }}/graphql/mutations/remove-coupon.html) to remove a discount coupon from the shopping cart. + +**Request:** + +```graphql +mutation { + removeCouponFromCart(input: { cart_id: "{ CART_ID }" }) { + cart { + applied_coupons { + code + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "removeCouponFromCart": { + "cart": { + "applied_coupons": { + "applied_coupon": null + } + } + } + } +} +``` diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-customer.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-customer.md new file mode 100644 index 00000000000..d428f0e0c73 --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-customer.md @@ -0,0 +1,100 @@ +--- +layout: tutorial +group: graphql +title: Step 1. Create a customer +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 10 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +This step creates a customer account and generates an authentication token for that customer. You can skip this step if you want to perform this tutorial as a guest user. + +## Create a customer + +Use the `createCustomer` mutation to register the new customer account in the store. + +**Request:** + +```graphql +mutation { + createCustomer( + input: { + firstname: "John" + lastname: "Doe" + email: "john.doe@example.com" + password: "b1b2b3l@w+" + is_subscribed: true + } + ) { + customer { + firstname + lastname + email + is_subscribed + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "createCustomer": { + "customer": { + "firstname": "John", + "lastname": "Doe", + "email": "john.doe@example.com", + "is_subscribed": true + } + } + } +} +``` + +The [`createCustomer`]({{ page.baseurl }}/graphql/mutations/create-customer.html) mutation describes additional parameters. + +## Generate an authentication token for the customer + +To place an order as a customer, you must obtain an authorization token by calling the `generateCustomerToken` mutation. You must include the customer's email and password as input. + +**Request:** + +```graphql +mutation { + generateCustomerToken(email: "john.doe@example.com", password: "b1b2b3l@w+") { + token + } +} +``` + +**Response:** + +```json +{ + "data": { + "generateCustomerToken": { + "token": "zuo7zor5jfldft2nmu2gtylnm8ui7e8t" + } + } +} +``` + +## Specify an Authorization header + +To send requests on behalf of the customer, you must supply the generated token as a header in your GraphQL browser. +The name of the header is `Authorization` and the value is `Bearer `. + +[Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) describes the mutation further. + +## Verify this step {#verify-step} + +Sign in as a customer to the website using the email `john.doe@example.com` and password `b1b2b3l@w+`. You should be successfully logged in. diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-payment-method.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-payment-method.md new file mode 100644 index 00000000000..8be733dd0d1 --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-payment-method.md @@ -0,0 +1,105 @@ +--- +layout: tutorial +group: graphql +title: Step 9. Set the payment method +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 90 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +{:.bs-callout-tip} +You must always set a payment method for an order. + +Use the following `cart` query to determine which payment methods which are available for your order. + +`{ CART_ID }` is the unique shopping cart ID from [Step 2. Create empty cart]({{ page.baseurl }}/graphql/tutorials/checkout/checkout-add-product-to-cart.html). + +For logged-in customers, send the customer's authorization token in the `Authorization` parameter of the header. See [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) for more information. + +**Request:** + +```graphql +query { + cart(cart_id: "{ CART_ID }") { + available_payment_methods { + code + title + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "cart": { + "available_payment_methods": [ + { + "code": "checkmo", + "title": "Check / Money order" + } + ] + } + } +} +``` + +### Set payment method on cart {#setPaymentMethodOnCart} + +Use the `setPaymentMethodOnCart` mutation to set the payment method for your order. The value `checkmo` ("Check / Money order" payment method code) was returned in the query. + +Send the customer's authorization token in the `Authorization` parameter of the header. See [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) for more information. + +**Request:** + +```graphql +mutation { + setPaymentMethodOnCart(input: { + cart_id: "{ CART_ID }" + payment_method: { + code: "checkmo" + } + }) { + cart { + selected_payment_method { + code + } + } + } +} +``` + +**Response:** + +If the operation is successful, the response contains the code of the selected payment method. + +```json +{ + "data": { + "setPaymentMethodOnCart": { + "cart": { + "selected_payment_method": { + "code": "checkmo" + } + } + } + } +} +``` + +## Verify this step {#verify-step} + +1. Sign in as a customer to the website using the email `john.doe@example.com` and password `b1b2b3l@w+`. + +1. Go to Checkout. + +1. The selected payment method is displayed in the Payment Method section on the Review & Payments step. diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-place-order.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-place-order.md new file mode 100644 index 00000000000..e0da5b09f45 --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-place-order.md @@ -0,0 +1,53 @@ +--- +layout: tutorial +group: graphql +title: Step 10. Place the order +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 100 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +The `placeOrder` mutation places an order. + +`{ CART_ID }` is the unique shopping cart ID from [Step 2. Create empty cart]({{ page.baseurl }}/graphql/tutorials/checkout/checkout-add-product-to-cart.html). + +Send the customer's authorization token in the `Authorization` parameter of the header. See [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) for more information. + +**Request:** + +```graphql +mutation { + placeOrder(input: {cart_id: "{ CART_ID }"}) { + order { + order_number + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "placeOrder": { + "order": { + "order_number": "000000001" + } + } + } +} +``` + +## Verify this step {#verify-step} + +1. Sign in as a customer to the website using the email `john.doe@example.com` and password `b1b2b3l@w+`. + +1. Go to **My Account** > **My Orders**. The order you created is displayed. The order is also displayed on the Orders grid (**Sales** > **Orders**) in the Admin. diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-quote-email.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-quote-email.md new file mode 100644 index 00000000000..cd2b14cd9f7 --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-quote-email.md @@ -0,0 +1,55 @@ +--- +layout: tutorial +group: graphql +title: Step 8. Set email on the cart (guest customers only) +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 80 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +{:.bs-callout-tip} +Skip this step if you placed the order as a registered customer. + +If you place an order as a guest user, you must set a quote email address. Use the `setGuestEmailOnCart` mutation query for that. + +`{ CART_ID }` is the unique shopping cart ID from [Step 2. Create empty cart]({{ page.baseurl }}/graphql/tutorials/checkout/checkout-add-product-to-cart.html). + +**Request:** + +```graphql +mutation { + setGuestEmailOnCart(input: { + cart_id: "{ CART_ID }" + email: "guest@example.com" + }) { + cart { + email + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setGuestEmailOnCart": { + "cart": { + "email": "guest@example.com" + } + } + } +} +``` + +## Verify this step {#verify-step} + +There are no additional verification steps. `quote`.`customer_email` is displayed for administrator on back-end side. diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-shipping-address.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-shipping-address.md new file mode 100644 index 00000000000..57a7f1d4571 --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-shipping-address.md @@ -0,0 +1,143 @@ +--- +layout: tutorial +group: graphql +title: Step 4. Set the shipping address +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 40 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +Use the [setShippingAddressesOnCart]({{ page.baseurl }}/graphql/mutations/set-shipping-address.html) mutation to set a shipping address. + +## Add shipping address to the cart + +In this step, we use the `setShippingAddressesOnCart` mutation to add a shipping address to the cart. + +If using guest checkout, run the following example. + +If using a logged in customer, send the customer's authorization token in the `Authorization` parameter of the header. See [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) for more information. + +**Request:** + +```graphql +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "{ CART_ID }" + shipping_addresses: [ + { + address: { + firstname: "John" + lastname: "Doe" + company: "Company Name" + street: ["3320 N Crescent Dr", "Beverly Hills"] + city: "Los Angeles" + region: "CA" + region_id: 12 + postcode: "90210" + country_code: "US" + telephone: "123-456-0000" + save_in_address_book: false + } + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + region { + code + label + } + postcode + telephone + country { + code + label + } + available_shipping_methods{ + carrier_code + carrier_title + method_code + method_title + } + } + } + } +} +``` + +**Response:** + +`setShippingAddressesOnCart` returns the new address details. + +```json +{ + "data": { + "setShippingAddressesOnCart": { + "cart": { + "shipping_addresses": [ + { + "firstname": "John", + "lastname": "Doe", + "company": "Company Name", + "street": [ + "3320 N Crescent Dr", + "Beverly Hills" + ], + "city": "Los Angeles", + "region": { + "code": "CA", + "label": "California" + }, + "postcode": "90210", + "telephone": "123-456-0000", + "country": { + "code": "US", + "label": "US" + }, + "available_shipping_methods": [ + { + "carrier_code": "flatrate", + "carrier_title": "Flat Rate", + "method_code": "flatrate", + "method_title": "Fixed" + }, + { + "carrier_code": "tablerate", + "carrier_title": "Best Way", + "method_code": "bestway", + "method_title": "Table Rate" + } + ] + } + ] + } + } + } +} +``` + +`{ CART_ID }` is the unique shopping cart ID from [Step 2. Create empty cart]({{ page.baseurl }}/graphql/tutorials/checkout/checkout-add-product-to-cart.html). + +Note the `available_shipping_methods` in the response. We will use this information in a later step. + +## Verify this step {#verify-step} + +1. Sign in as a customer to the website using the email `john.doe@example.com` and password `b1b2b3l@w+`. + +1. Go to Checkout. + +1. On the Shipping step, the Shipping Address form contains the address details you entered. diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-shipping-method.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-shipping-method.md new file mode 100644 index 00000000000..261627b1fe8 --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-shipping-method.md @@ -0,0 +1,86 @@ +--- +layout: tutorial +group: graphql +title: Step 6. Set the shipping method +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 60 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +The `setShippingMethodsOnCart` mutation defines the shipping methods for your order. It requires these input parameters: + +* `cart_id` +* `carrier_code` +* `method_code` + +`{ CART_ID }` is the unique shopping cart ID from [Step 2. Create empty cart]({{ page.baseurl }}/graphql/tutorials/checkout/checkout-add-product-to-cart.html). + +{:.bs-callout-info} +For logged-in customers, send the customer's authorization token in the `Authorization` parameter of the header. See [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) for more information. + +**Request:** + +The following mutation query assigns UPS "Ground" method. + +```text +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{ CART_ID }" + shipping_methods: [ + { + carrier_code: "ups" + method_code: "GND" + } + ] + }) { + cart { + shipping_addresses { + selected_shipping_method { + carrier_code + method_code + carrier_title + method_title + } + } + } + } +} +``` + +**Response:** + +```json +{ + "data": { + "setShippingMethodsOnCart": { + "cart": { + "shipping_addresses": [ + { + "selected_shipping_method": { + "carrier_code": "ups", + "method_code": "GND", + "carrier_title": "United Parcel Service", + "method_title": "Ground" + } + } + ] + } + } + } +} +``` + +## Verify this step {#verify-step} + +1. Sign in as a customer to the website using the email `john.doe@example.com` and password `b1b2b3l@w+`. + +1. Go to Checkout. + +1. The selected shipping method is displayed in the Shipping Methods section on the Shipping step. diff --git a/src/guides/v2.3/graphql/tutorials/checkout/checkout-shopping-cart.md b/src/guides/v2.3/graphql/tutorials/checkout/checkout-shopping-cart.md new file mode 100644 index 00000000000..4a4404e7f45 --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/checkout-shopping-cart.md @@ -0,0 +1,78 @@ +--- +layout: tutorial +group: graphql +title: Step 2. Create an empty cart +subtitle: GraphQL checkout tutorial +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +menu_order: 20 +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- +The procedure for creating a cart varies for logged-in customers and guests. + +The `customerCart` query returns the active cart for the logged-in customer. If the cart does not exist, the query creates one. You must specify the customer’s authorization token in the headers. Otherwise, the query fails. ["Get customer authorization token"]({{ page.baseurl }}/graphql/authorization-tokens.html) describes these tokens. + +For guests, use the [`createEmptyCart`]({{page.baseurl}}/graphql/mutations/create-empty-cart.html) mutation to create an empty shopping cart and generate a cart ID for a guest user. If the guest later logs in as a customer, use the [`mergeCarts`]({{page.baseurl}}/graphql/mutations/merge-carts.html) mutation to transfer the contents of the guest cart into the customer's cart. + +## Create a customer cart + +The customer created in the previous step does not have an active cart. The following query creates an empty cart and returns the cart ID. You must specify the customer’s authorization token in the headers of the call. + +**Request:** + +```graphql +{ + customerCart{ + id + } +} +``` + +**Response:** + +```json +{ + "data": { + "customerCart": { + "id": "pXVxnNg4PFcK1lD60O5evqF7f4SkiRR1" + } + } +} +``` + +In the subsequent tutorial steps, the unique shopping cart identifier `pXVxnNg4PFcK1lD60O5evqF7f4SkiRR1` will be listed as `{ CART_ID }`. +Copy the value of the id attribute. Use this value in subsequent steps wherever the { CART_ID } variable is specified. + +## Create a guest cart + +The following example creates an empty cart for a guest. Do not include an authorization token on any call made on behalf of a guest. + +**Request:** + +```graphql +mutation { + createEmptyCart +} +``` + +**Response:** + +```json +{ + "data": { + "createEmptyCart": "A7jCcOmUjjCh7MxDIzu1SeqdqETqEa5h" + } +} +``` + +In the subsequent tutorial steps, the unique shopping cart identifier `A7jCcOmUjjCh7MxDIzu1SeqdqETqEa5h` will be listed as `{ CART_ID }`. +Copy the value of the id attribute. Use this value in subsequent steps wherever the { CART_ID } variable is specified. + +## Verify this step {#verify-step} + +There are no additional verification steps. The value of `id` is not displayed on the website or in the Admin. diff --git a/src/guides/v2.3/graphql/tutorials/checkout/index.md b/src/guides/v2.3/graphql/tutorials/checkout/index.md new file mode 100644 index 00000000000..8556e41646c --- /dev/null +++ b/src/guides/v2.3/graphql/tutorials/checkout/index.md @@ -0,0 +1,45 @@ +--- +layout: tutorial +group: graphql +title: GraphQL checkout tutorial +menu_title: Initial tasks +menu_order: 0 +level3_subgroup: graphql-checkout +return_to: + title: GraphQL Overview + url: graphql/index.html +functional_areas: + - Integration +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +--- + +This tutorial describes how to place an order through GraphQl. Customers can make purchases in two ways: + +- As a logged-in user +- As a guest user who does not create an account + +The **10-step tutorial** generally takes **30 minutes**. + +Magento GraphQL is designed to run queries and perform actions on behalf of a customer. Magento GraphQL does not perform backend tasks, such as manage invoices or shipments. + +### Before you begin + +Complete the following prerequisites: + +- Install a Magento 2 instance with sample data. + The sample data defines a functional store, called Luma, that sells fitness clothing and accessories. The store does not provide any sandbox accounts for testing credit card payments, so transactions will be simulated using an offline [payment method](https://glossary.magento.com/payment-method). + +- Install a GraphQl client. You can use any GraphQl client to send calls to Magento. [Altair](https://altair.sirmuel.design/) is a good example. + +- Learn about GraphQL, how it works, and how to use it. See [Introduction to GraphQL](https://graphql.org/learn/) for details. + +- Know how to generate a customer token. See [Authorization tokens]({{page.baseurl}}/graphql/authorization-tokens.html) for details. + +- In the Magento admin, create a [coupon]({{ site.user_guide_url }}/marketing/price-rules-cart-coupon-code-configure.html) that will be used in [Step 7. Apply a coupon]({{page.baseurl}}/graphql/tutorials/checkout/checkout-coupon.html). + +### Other resources + +- [Order processing tutorial]({{ page.baseurl }}/rest/tutorials/orders/order-intro.html) shows a system integrator how REST APIs are used in the lifecycle of an order, including configuring a store and creating a customer; creating quotes, orders, invoices, and shipments; preparing for checkout; and more order-related tasks. + +- [REST Tutorials]({{ page.baseurl }}/rest/tutorials/index.html) provides additional information about completing any Magento REST tutorial. diff --git a/src/guides/v2.3/howdoi/admin/customize_admin.md b/src/guides/v2.3/howdoi/admin/customize_admin.md new file mode 100644 index 00000000000..c5364c29060 --- /dev/null +++ b/src/guides/v2.3/howdoi/admin/customize_admin.md @@ -0,0 +1,233 @@ +--- +group: how-do-i +title: Customize the design +--- + +Learn how to customize (add, delete, change) the configuration options available in the Admin. These options define the various aspects of storefront design. + +The location of your customizations appears here: + +- {{site.data.var.ee}} and {{site.data.var.ce}} 2.1.0 and later: Options set under **CONTENT > Design > Configuration** + +## Environment and technologies + +- {{ site.data.var.ee }} +- {{ site.data.var.ece }} +- {{ site.data.var.ce }} + +## Prerequisites + +- Magento installed +- Credentials or access to Admin + +## Steps + + {:.bs-callout-info} +The following walk-through uses version 2.1.0 and later. + +On a default Magento installation, when you navigate to **CONTENT > Design > Configuration** in Admin, the first page that opens displays a grid with the available configuration scopes and assigned themes. It looks like following: + +![Design Configuration]({{ site.baseurl }}/common/images/design_conf1.png) + +When you click **Edit** in any of the scope records, the page with available design options is displayed. For example, the default set of design options for the store view level is the following: + +![Default Store View]({{ site.baseurl }}/common/images/design_conf2.png) + +Both the grid and the configuration form are implemented using UI components. + +To change the grid view, you must [customize the grid](#customize-the-grid) configuration by adding a custom `design_config_listing.xml` file to your module. + +To change the available design settings, you must [customize the grid](#customize-the-grid) configuration by adding your custom `design_config_form.xml` file in your module. If you add a new field, you must also declare it in the `di.xml` file. + +## Customize the grid + +The grid containing the configuration scopes is implemented using the [grid UI component]({{page.baseurl}}/ui_comp_guide/components/ui-listing-grid.html). + +To customize the grid view, take the following steps: + +1. In the `/view/adminhtml/ui_component` directory, add the empty `design_config_listing.xml` file. + +1. In the `design_config_listing.xml` file, create an element to which to add your customizations. For example, if you want to rename the column displaying the selected theme, your grid configuration must contain the following: + +```xml + + + + + + + + %New theme column name% + + + + + +``` + +Your `design_config_listing.xml` file is merged with the same files from the other modules, so there is no need to copy their contents. You only need to define changes. Even if you want to customize the existing entities, you only need to mention those options for which the values are customized. + +For reference, view the grid configuration files of the Magento modules: + +- `/view/adminhtml/ui_component/design_config_listing.xml` +- `/view/adminhtml/ui_component/design_config_listing.xml` + +If you add a certain field as an additional grid column, you must also set the field’s `use_in_grid` property in the [field’s meta data](#add-fields-metadata) in the `di.xml` file. + +## Customize the design options + +These sections detail how to customize form configuration and field metadata. + +### Customize the form configuration + +The design configuration form is implemented using the form UI component. + +To customize the form view, take the following steps: + +1. Create an empty `design_config_form.xml` file in the `/view/adminhtml/ui_component/` directory. + +1. Add content similar to the following: + +```xml +
    + + +
    + + + %Fieldset Label as displayed in UI% + %order for displaying% + + + +
    + + + %Nested fieldset Label as displayed in UI% + true + + %level of nesting% + + + + + + %value% + %value% + .... + + + +
    +
    +
    +``` + +Your custom fields and field sets will be available for all configuration scopes (website, store, and store view). + +Your `design_config_form.xml` file is merged with the same files from the other modules, so there is no need to copy their contents You only need to add your customizations. + +To customize an existing entity, declare only those options; the values of which are customized. Do not copy its entire configuration. + +For example, if you only want to rename the **Other Settings** field set, your form configuration must contain the following: + +```xml + + +
    + +
    + + + Other Settings + + + +
    +
    +``` + +To delete an existing field, or field set, in your `design_config_form.xml` file, use the following syntax: + +```xml +... +
    + + true + +
    +... +``` + +For reference, view the form configuration files of these Magento modules: + +- `/view/adminhtml/ui_component/design_config_form.xml` +- `/view/adminhtml/ui_component/design_config_form.xml` +- `/view/adminhtml/ui_component/design_config_form.xml` +- `/view/adminhtml/ui_component/design_config_form.xml` +- `/view/adminhtml/ui_component/design_config_form.xml` + +### Add fields’ metadata + +If in the design configuration form you add new fields, `/etc/di.xml`, you must specify their parent field sets and the path in the database. You can also declare the backend model used for processing the field values. If you do not specify any model, the default `Magento\Framework\App\Config\Value` model is used. + +The field declaration in a `di.xml` file looks like the following: + +```xml +... + + + + + + + %path in system config% + + %parent_fieldset% + + %Backend\Model\For\\Field\Processing% + + true|false + + + + + + + + + +... +``` + +Example of a field declaration: + +```xml + + + + + design/head/shortcut_icon + head + Magento\Config\Model\Config\Backend\Image\Favicon + + media + 1 + favicon + + + + + +``` +For more examples and reference, view the `di.xml` files of these Magento modules: + +- `/etc/di.xml` +- `/etc/di.xml` +- `/etc/di.xml` +- `/etc/di.xml` +- `/etc/di.xml` + +### Accessing the options values in backend models + +The design configuration option values are stored in the `core_config_data` table in the database, similar to the values of System Configuration options, and can be accessed using the `\Magento\Framework\App\ConfigInterface` mechanism. diff --git a/src/guides/v2.3/howdoi/checkout/checkout-add-custom-carrier.md b/src/guides/v2.3/howdoi/checkout/checkout-add-custom-carrier.md new file mode 100644 index 00000000000..8cfb842da6b --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout-add-custom-carrier.md @@ -0,0 +1,308 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add custom shipping carrier +subtitle: Customize Checkout +contributor_name: Atwix +contributor_link: https://www.atwix.com/ +menu_order: 5 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how to add a custom shipping carrier. + +To add a new shipping carrier to the Magento checkout: + +1. [Create a new module](#create-module) +1. [Add the carrier configuration](#create-configuration) +1. [Create the carrier model](#carrier-model) +1. [Enable the module](#enable-module) + +## Step 1: Create a new module {#create-module} + +The example module for use here is `Vendor_CustomShipping`. + +### Source code of `app/code/Vendor/CustomShipping/registration.php` + +```php + + + + + + + + + + + +``` + +## Step 2: Add the module configuration {#create-configuration} + +To add a module configuration use the following source code snippets. + +### Source code of `app/code/Vendor/CustomShipping/etc/adminhtml/system.xml` + +The `system.xml` source code declares custom shipping module options: + +- Enabled +- Title +- Method Name +- Shipping Cost +- Ship to Applicable Countries +- Ship to Specific Countries +- Show Method if Not Applicable +- Sort Order + +```xml + + + +
    + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + + + + + + validate-number validate-zero-or-greater + + + + shipping-applicable-country + Magento\Shipping\Model\Config\Source\Allspecificcountries + + + + Magento\Directory\Model\Config\Source\Country + 1 + + + + Magento\Config\Model\Config\Source\Yesno + shipping-skip-hide + + + + + +
    +
    +
    +``` + +### Source code of `app/code/Vendor/CustomShipping/etc/config.xml` + +The `config.xml` file specifies default values for custom shipping module options and the shipping module model, `Vendor\CustomShipping\Model\Carrier\Customshipping`: + +```xml + + + + + + 0 + Custom Shipping Title + Custom Shipping Method Name + 10 + 0 + 15 + Vendor\CustomShipping\Model\Carrier\Customshipping + + + + +``` + +## Step 3: Create the carrier model {#carrier-model} + +In this example, the `Vendor\CustomShipping\Model\Carrier\Customshipping` class is a skeleton of a carrier model. You can extend it to fit your needs. + +The carrier class implements the `CarrierInterface` interface and retrieves all available shipping methods in the `getAllowedMethods` function. The `collectRates` function returns the `\Magento\Shipping\Model\Rate\Result` object if the carrier method is available on checkout. Otherwise, it returns `false`---the carrier method is not applicable to the shopping cart. + +### Source code of `app/code/Vendor/CustomShipping/Model/Carrier/Customshipping.php` + +```php +rateResultFactory = $rateResultFactory; + $this->rateMethodFactory = $rateMethodFactory; + } + + /** + * Custom Shipping Rates Collector + * + * @param RateRequest $request + * @return \Magento\Shipping\Model\Rate\Result|bool + */ + public function collectRates(RateRequest $request) + { + if (!$this->getConfigFlag('active')) { + return false; + } + + /** @var \Magento\Shipping\Model\Rate\Result $result */ + $result = $this->rateResultFactory->create(); + + /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */ + $method = $this->rateMethodFactory->create(); + + $method->setCarrier($this->_code); + $method->setCarrierTitle($this->getConfigData('title')); + + $method->setMethod($this->_code); + $method->setMethodTitle($this->getConfigData('name')); + + $shippingCost = (float)$this->getConfigData('shipping_cost'); + + $method->setPrice($shippingCost); + $method->setCost($shippingCost); + + $result->append($method); + + return $result; + } + + /** + * @return array + */ + public function getAllowedMethods() + { + return [$this->_code => $this->getConfigData('name')]; + } +} +``` + +## Step 4: Enable the module {#enable-module} + +Run the commands below to register `Vendor_CustomShipping` module: + +```bash +bin/magento module:enable Vendor_CustomShipping +``` + +```bash +bin/magento setup:upgrade +``` + +## Screenshots + +The backend settings for the custom shipping carrier appear as shown below. + +![Custom shipping carrier backend settings]({{ page.baseurl }}/howdoi/checkout/images/checkout-add-custom-carrier-01.png) + +The custom shipping carrier will appear on checkout as shown below. + +![Custom shipping carrier on checkout]({{ page.baseurl }}/howdoi/checkout/images/checkout-add-custom-carrier-02.png) diff --git a/src/guides/v2.3/howdoi/checkout/checkout_address.md b/src/guides/v2.3/howdoi/checkout/checkout_address.md new file mode 100644 index 00000000000..197cbc6fb08 --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_address.md @@ -0,0 +1,347 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add a custom shipping address renderer +subtitle: Customize Checkout +menu_order: 11 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how to implement a custom shipping address renderer. + +Out of the box, Magento [checkout](https://glossary.magento.com/checkout) consists of two steps: + +- Shipping Information +- Review and Payment Information + +On the Shipping Information checkout step Magento renders all addresses previously saved by a shopper. The shopper can then select the one to be used for shipping by clicking it. The default address renderers cover the majority of use cases, but Magento provides way to register custom address renderer for a new address type. + +To implement shipping address rendering in checkout, you need to take the following steps: + +1. [Create the JS renderer component (shipping address renderer)](#create). +1. [Create a template for the shipping address renderer.](#template) +1. [Create the JS model for the shipping rate processor](#rate_processor). +1. [Create the JS model for the shipping address saving processor](#save). +1. [Create the JS component registering the processors](#register). +1. [Declare the new components in the checkout page layout.](#layout). +1. [Add the shipping address renderer to the "Ship-To" block (optional)](#ship_to). + +## Step 1: Create the JS renderer component (shipping address renderer) {#create} + +Your shipping address renderer must be implemented as a [JavaScript](https://glossary.magento.com/javascript) [UI component](https://glossary.magento.com/ui-component). That is, it must be a RequireJS module, and must return a factory function, that takes a configurable object. + +For the sake of compatibility, upgradability and easy maintenance, do not edit the default Magento code. Instead add your customizations in a separate module. For your checkout customization to be applied correctly, your custom module must depend on the `Magento_Checkout` module. Module dependencies are specified in the [module's `composer.json`]({{ page.baseurl }}/extension-dev-guide/build/composer-integration.html). Do not use `Ui` for your custom module name, because `%Vendor%_Ui` notation, required when specifying paths, might cause issues. + +In your custom module directory, create the component's `.js` file (shipping address renderer). It must be located under the `/view/frontend/web/js/view/` directory. + +The following is a general view of the shipping address renderer: + +```js +define([ + 'ko', + 'uiComponent', + 'Magento_Checkout/js/action/select-shipping-address', + 'Magento_Checkout/js/model/quote' +], function(ko, Component, selectShippingAddressAction, quote) { + 'use strict'; + return Component.extend({ + defaults: { + template: '%path to your template%' + }, + + initProperties: function () { + this._super(); + this.isSelected = ko.computed(function() { + var isSelected = false; + var shippingAddress = quote.shippingAddress(); + if (shippingAddress) { + isSelected = shippingAddress.getKey() == this.address().getKey(); + } + return isSelected; + }, this); + + return this; + }, + + /** Set selected customer shipping address */ + selectAddress: function() { + selectShippingAddressAction(this.address()); + }, + + /** additional logic required for this renderer **/ + + }); +}); +``` + +## Step 2: Create a template for the shipping address renderer {#template} + +In your custom module directory, create a new `/view/frontend/web/template/.html` file. The template can use [Knockout JS](https://knockoutjs.com/) syntax. + +The template should contain a button for setting the address to be used for shipping. + +You can use the code from the default template: [app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Checkout/view/frontend/web/template/shipping-address/address-renderer/default.html). + +## Step 3: Create the JS model for the shipping rate processor {#rate_processor} + +A shipping rate processor is responsible for retrieving the shipping rates available for the given shipping address. + +In your custom module directory, create the component's `.js` file for the processor. It must be located under the `/view/frontend/web/js/model/` directory. + +Here you need to specify the [URL](https://glossary.magento.com/url) used for calculating the shipping rates for your custom address type. + +The following is a sample of the shipping rate processor code: + +```js +define( + [ + 'Magento_Checkout/js/model/quote', + 'Magento_Checkout/js/model/shipping-service', + 'Magento_Checkout/js/model/shipping-rate-registry', + 'magento/storage', + 'Magento_Checkout/js/model/error-processor', + // additional dependencies + ], + function (quote, shippingService, rateRegistry, storage, errorProcessor, ...) { + 'use strict'; + return { + getRates: function(address) { + var cache = rateRegistry.get(address.getKey()); + if (cache) { + shippingService.setShippingRates(cache); + } else { + shippingService.isLoading(true); + storage.post( + %URL for shipping rate estimation%, + JSON.stringify({ + %address parameters% + }), + false + ).done( + function (result) { + rateRegistry.set(address.getKey(), result); + shippingService.setShippingRates(result); + } + ).fail( + function (response) { + shippingService.setShippingRates([]); + errorProcessor.process(response); + } + ).always( + function () { + shippingService.isLoading(false); + } + ); + } + } + }; + } +); +``` + +## Step 4: Create the JS model for the shipping address saving processor {#save} + +This processor is responsible for sending the shipping address and the selected rate to the server. + +In your custom module directory, create the component's `.js` file for the processor. It must be located under the `/view/frontend/web/js/model/` directory. + +The following is a sample of the shipping rate processor code: + +```js +define( + [ + 'Magento_Checkout/js/model/quote', + 'Magento_Checkout/js/model/resource-url-manager', + 'mage/storage', + 'Magento_Checkout/js/model/payment-service', + 'Magento_Checkout/js/model/error-processor', + 'Magento_Checkout/js/model/payment/method-converter' + ], + function (quote, resourceUrlManager, storage, paymentService, errorProcessor, methodConverter) { + 'use strict'; + return { + saveShippingInformation: function() { + var shippingAddress = {}, + payload; + + shippingAddress.extension_attributes = { + %address extension attributes% + }; + + payload = { + addressInformation: { + shipping_address: shippingAddress, + shipping_method_code: quote.shippingMethod().method_code, + shipping_carrier_code: quote.shippingMethod().carrier_code + } + }; + + return storage.post( + resourceUrlManager.getUrlForSetShippingInformation(quote), + JSON.stringify(payload) + ).done( + function (response) { + paymentService.setPaymentMethods(methodConverter(response.payment_methods)); + quote.setTotals(response.totals) + } + ).fail( + function (response) { + errorProcessor.process(response); + } + ); + } + } + } +); +``` + +## Step 5: Create the JS component registering the processors {#register} + +In your custom module directory, create the `.js` UI component that registers the rate processor and the saving processor. It must be located under the `/view/frontend/web/js/view/` directory. + +The file content must be similar to the following: + +```js +define( + [ + 'uiComponent', + 'Magento_Checkout/js/model/shipping-rate-service', + %custom shipping rate processor%, + 'Magento_Checkout/js/model/shipping-save-processor', + %custom shipping save processor% + ], + function ( + Component, + shippingRateService, + customShippingRateProcessor, + shippingSaveProcessor, + customShippingSaveProcessor + ) { + 'use strict'; + + /** Register rate processor */ + shippingRateService.registerProcessor(%address type%, customShippingRateProcessor); + + /** Register save shipping address processor */ + shippingSaveProcessor.registerProcessor(%address type%, custormShippingSaveProcessor); + + /** Add view logic here if needed */ + return Component.extend({}); + } +); +``` + +## Step 6: Declare the new components in the checkout page layout {#layout} + +In your custom module directory, create a new `/view/frontend/layout/checkout_index_index.xml` file. In this file, add the following: + +```xml + + + + + + + + + + + + + + + %component that registers address/rate processors% + + + + + + + %address renderer JS component% + + + + + + + + + + + + + + + + +``` + +The `address_type` you need to specify in the layout, is the value you set in the JS model of your custom address type. + +## Step 7: Add the shipping address renderer to the "Ship-To" block (optional) {#ship_to} + +On the Review and Payment Information step of checkout, the shipping address is displayed in the **Ship-To** section for customer to make sure everything is set correctly. + +If you want your custom address type to be displayed here as well, you need to create an `.html` template for rendering it, and declare in the corresponding location in [layout](https://glossary.magento.com/layout). + +### Add template for displaying the address in the Ship-To section + +In your custom module directory create a new `/view/frontend/web/template/.html` file. The template can use [Knockout JS](https://knockoutjs.com/) syntax. + +You can use the code from the default template: [app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Checkout/view/frontend/web/template/shipping-information/address-renderer/default.html). + +### Declare the address to be used in the Ship-To section in layout + +In your `/view/frontend/layout/checkout_index_index.xml` file, add the following: + +```xml + + + + + + + + + + + + + + + uiComponent + + %custom template% + + + + + + + + + + + + + + + +``` + +## Step 8: Deploy the static content and clean the cache + +1. Deploy static content: + + ```bash + bin/magento setup:static-content:deploy + ``` + +1. Then clean the cache: + + ```bash + bin/magento cache:clean + ``` diff --git a/src/guides/v2.3/howdoi/checkout/checkout_carrier.md b/src/guides/v2.3/howdoi/checkout/checkout_carrier.md new file mode 100644 index 00000000000..a9409259a3f --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_carrier.md @@ -0,0 +1,194 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add custom shipping carrier validations +subtitle: Customize Checkout +menu_order: 6 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how to add shipping address validations for a custom [shipping carrier](https://glossary.magento.com/shipping-carrier) to the Magento [checkout](https://glossary.magento.com/checkout). This is an essential part of the bigger task of adding a custom shipping method to your Magento store. + +To add new shipping carrier validations to the Magento checkout, do the following: + +1. [Create validation rules](#rules). +1. [Create validator](#validator). +1. [Register validator and rules in the validators pool](#register). +1. [Add the validators and rules to the checkout layout](#layout). + + {:.bs-callout-info} +During checkout, when a customer fills the shipping address form, shipping carrier validations trigger the shipping rates request. That is why adding shipping carrier validations for your custom shipping method is mandatory. + +## Step 1: Create validation rules {#rules} + +Shipping carrier validation rules declare which fields of the shipping address are required for the corresponding shipping method to be available. The validation itself is performed by the [validator](#validator). + +During checkout, if the shipping address fields declared in the rules are filled, the further validation of fields' values is carried on the [server side](https://glossary.magento.com/server-side). For example, whether a carrier is available for the specified country. + +For the sake of compatibility, upgradability, and easy maintenance, do not edit the default Magento code, add your customization in a separate module. For your checkout customization to be applied correctly, your custom module should [depend]({{ page.baseurl }}/extension-dev-guide/build/composer-integration.html) on the `Magento_Checkout` module. + +Do not use `Ui` for your custom module name, because `%Vendor%_Ui` notation, required when specifying paths, might cause issues. + +In your `/view/frontend/web/js/model` directory, create a `.js` file implementing the validation rules. + +The script must implement the `getRules()` method. + +For example, the FedEx shipping method requires only two fields of the shipping address to be filled: **Country**, **Zip Code** and **City**. This is how the validation rules for FedEx look: + +> _/view/frontend/web/js/model/shipping-rates-validation-rules.js_ + +```js +define( + [], + function () { + 'use strict'; + return { + getRules: function() { + return { + 'postcode': { + 'required': true + }, + 'country_id': { + 'required': true + }, + 'city': { + 'required': true + } + }; + } + }; + } +) +``` + +Triggering the shipping rates request correlates directly with the fields you specify in the validation rules: the request is triggered once all these fields are populated and pass the validation. + +## Step 2: Create validator {#validator} + +Create the validator `.js` script that checks if the fields defined by the validation rules are filled. The script must be located in the `/view/frontend/web/js/model` directory. + +A sample validator script follows: + +```js +define( + [ + 'jquery', + 'mageUtils', + + './shipping-rates-validation-rules', + 'mage/translate' + ], + function ($, utils, validationRules, $t) { + 'use strict'; + return { + validationErrors: [], + validate: function(address) { + var self = this; + this.validationErrors = []; + $.each(validationRules.getRules(), function(field, rule) { + if (rule.required && utils.isEmpty(address[field])) { + var message = $t('Field ') + field + $t(' is required.'); + self.validationErrors.push(message); + } + }); + return !Boolean(this.validationErrors.length); + } + }; + } +); +``` + +You can use this sample for your validator, but you need to specify your validation rules script instead of `./shipping-rates-validation-rules` in the list of used modules. + +## Step 3: Register validator and rules in the validators pool {#register} + +Your custom validator must be added to the pool of validators. To do this, in the `/view/frontend/web/js/view` directory create a new `.js` file with the following content (having replaced the `` and `` with your values): + +```js +define( + [ + 'uiComponent', + 'Magento_Checkout/js/model/shipping-rates-validator', + 'Magento_Checkout/js/model/shipping-rates-validation-rules', + '../model/', + '../model/' + ], + function ( + Component, + defaultShippingRatesValidator, + defaultShippingRatesValidationRules, + shippingRatesValidator, + shippingRatesValidationRules + ) { + 'use strict'; + defaultShippingRatesValidator.registerValidator('carrierName', shippingRatesValidator); + defaultShippingRatesValidationRules.registerRules('carrierName', shippingRatesValidationRules); + return Component; + } +); +``` + +## Step 4: Add the validation to the checkout layout {#layout} + +The last step is specifying the script you created on the previous step in the checkout page [layout](https://glossary.magento.com/layout). + +In your custom module directory, create a new `/view/frontend/layout/checkout_index_index.xml` file. + +In this file, add the following: + +You must add `` like `%carrier%-rates-validation` - where carrier has to match the actual carrier code. + +```xml + + + + + + + + + + + + + + + + + + %your_module%/js/view/%your-validation% + + + + + + + + + + + + + + + + + +``` + +## Step 5: Deploy static content and clear the cache {#deploy-and-clean} + +1. Deploy static content: + +```bash +bin/magento setup:static-content:deploy +``` + +1. Clean the cache: + +```bash +bin/magento cache:clean +``` diff --git a/src/guides/v2.3/howdoi/checkout/checkout_custom_checkbox.md b/src/guides/v2.3/howdoi/checkout/checkout_custom_checkbox.md new file mode 100644 index 00000000000..0d12627b3ba --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_custom_checkbox.md @@ -0,0 +1,221 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add custom fields that influence other Checkout fields +subtitle: Customize Checkout +menu_order: 90 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how to add a custom field that influences other fields on the checkout page. + +Let's consider a case where you need to add a checkbox whose state (selected or cleared) changes the state of other fields: when the checkbox is selected, the Shipping Address fields get prepopulated with a certain address. + +To implement such a checkbox, take the following steps: + +1. [Create a plugin for the process method](#create-plugin) of the `/Block/Checkout/LayoutProcessor.php` class. +1. [Declare the plugin in your module's `di.xml`](#declare-plugin). +1. [Create a JS component for the checkbox with custom logic](#create-jscomponent). + +## Step 1: Create a plugin for the `LayoutProcessor`'s process method {#create-plugin} + +In your custom module directory, create the following new file: `/Block/Checkout/SomeProcessor.php`. In this file, add the following code sample. This is a plugin that adds a checkbox, makes the street labels trackable, and assigns dependencies to the checkbox. + +```php?start_inline=1 +namespace Magento\Checkout\Block\Checkout; + +class SomeProcessor +{ + /** + * Checkout LayoutProcessor after process plugin. + * + * @param \Magento\Checkout\Block\Checkout\LayoutProcessor $processor + * @param array $jsLayout + * @return array + */ + public function afterProcess(\Magento\Checkout\Block\Checkout\LayoutProcessor $processor, $jsLayout) + { + $shippingConfiguration = &$jsLayout['components']['checkout']['children']['steps']['children']['shipping-step'] + ['children']['shippingAddress']['children']['shipping-address-fieldset']['children']; + $billingConfiguration = &$jsLayout['components']['checkout']['children']['steps']['children']['billing-step'] + ['children']['payment']['children']['payments-list']['children']; + + //Checks if shipping step available. + if (isset($shippingConfiguration)) { + $shippingConfiguration = $this->processAddress( + $shippingConfiguration, + 'shippingAddress', + [ + 'checkoutProvider', + 'checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset.street', + 'checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset.city', + 'checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset.country_id' + ] + ); + } + + //Checks if billing step available. + if (isset($billingConfiguration)) { + //Iterate over billing forms. + foreach($billingConfiguration as $key => &$billingForm) { + //Exclude not billing forms + if (!strpos($key, '-form')) { + continue; + } + + $billingForm['children']['form-fields']['children'] = $this->processAddress( + $billingForm['children']['form-fields']['children'], + $billingForm['dataScopePrefix'], + [ + 'checkoutProvider', + 'checkout.steps.billing-step.payment.payments-list.' . $key . '.form-fields.street', + 'checkout.steps.billing-step.payment.payments-list.' . $key . '.form-fields.city', + 'checkout.steps.billing-step.payment.payments-list.' . $key . '.form-fields.country_id' + ] + ); + } + } + + return $jsLayout; + } + + /** + * Process provided address to contains checkbox and have trackable address fields. + * + * @param $addressFieldset - Address fieldset config. + * @param $dataScope - data scope + * @param $deps - list of dependencies + * @return array + */ + private function processAddress($addressFieldset, $dataScope, $deps) + { + //Creates checkbox. + $addressFieldset['custom_checkbox'] = [ + 'component' => 'Magento_Checkout/js/single-checkbox', + 'config' => [ + 'customScope' => $dataScope, + 'template' => 'ui/form/field', + 'prefer' => 'checkbox' + ], + 'dataScope' => $dataScope . '.custom_checkbox', + 'deps' => $deps, + 'label' => __('Army Address'), + 'provider' => 'checkoutProvider', + 'visible' => true, + 'initialValue' => false, + 'sortOrder' => 10, + 'valueMap' => [ + 'true' => true, + 'false' => false + ] + ]; + + //Makes each address field label trackable. + if (isset($addressFieldset['street']['children'])) { + foreach($addressFieldset['street']['children'] as $key => $street) { + $street['tracks']['label'] = true; + //Remove .additional class. Can be removed, but style fix provided instead. + $street['additionalClasses'] = ''; + $addressFieldset['street']['children'][$key] = $street; + } + } + + return $addressFieldset; + } +} +``` + +For more information on creating plugins, see [Plugins (Interceptors)]({{ page.baseurl }}/extension-dev-guide/plugins.html). + +## Step 2: Declare plugin in di.xml {#declare-plugin} + +In `/etc/frontend/di.xml`, declare the plugin you created on the previous step. The plugin name is arbitrary, in our example it's `ProcessAddressConfiguration`: + +```xml + + + + + +``` + +## Step 3: Create a JS component for the checkbox {#create-jscomponent} + +In your custom module directory, create the following new file: `/view/frontend/web/js/single-checkbox.js`. In this file, add the following code. This is a JS component that extends `Magento_Ui/js/form/element/single-checkbox.js`. The `onCheckedChanged` method calls the methods that update street labels, change the city and country values, and disable these fields: + +```js +define([ + 'Magento_Ui/js/form/element/single-checkbox', + 'mage/translate' +], function (AbstractField, $t) { + 'use strict'; + + return AbstractField.extend({ + defaults: { + streetLabels: [$t('Company / Section / Unit'), $t('Post Sector Type'), $t('Post Sector')], + modules: { + street: '${ $.parentName }.street', + city: '${ $.parentName }.city', + country: '${ $.parentName }.country_id' + } + }, + + updateStreetLabels: function () { + if (this.value()) { + this.street().elems.each(function (street, key) { + this.street().elems()[key].set('label', this.streetLabels[key]); + }.bind(this)); + } else { + this.street().elems.each(function (street, key) { + this.street().elems()[key].set('label', ''); + }.bind(this)); + } + }, + + updateCity: function () { + if (this.value()) { + this.city().value('Kyiv'); + this.city().disabled(true); + } else { + this.city().value(''); + this.city().disabled(false); + } + }, + + updateCountry: function () { + if (this.value()) { + this.country().value('UA'); + this.country().disabled(true); + } else { + this.country().value(''); + this.country().disabled(false); + } + }, + + onCheckedChanged: function () { + this._super(); + this.updateStreetLabels(); + this.updateCity(); + this.updateCountry(); + } + }); +}); +``` + +## Step 4: Compile and deploy the static content + +1. Compile the code: + + ```bash + bin/magento setup:di:compile + ``` + +1. Deploy static content: + + ```bash + bin/magento setup:static-content:deploy + ``` diff --git a/src/guides/v2.3/howdoi/checkout/checkout_customize.md b/src/guides/v2.3/howdoi/checkout/checkout_customize.md new file mode 100755 index 00000000000..e258f985bca --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_customize.md @@ -0,0 +1,200 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Customize the view of a checkout step +subtitle: Customize Checkout +menu_order: 2 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic contains the basic information about how to customize the view of an existing [checkout](https://glossary.magento.com/checkout) step. In the Magento application, checkout is implemented using UI components. You can customize each step by [changing the JavaScript implementation or template](#change-component) for a component, [adding](#add), [disabling](#disable), or [removing](#remove) a component. + +## Change the component's .js implementation and template {#change-component} + +To change the `.js` implementation and template used for components rendering, you need to declare the new files in the checkout page [layout](https://glossary.magento.com/layout). To do this, take the following steps: + +1. In your custom module directory, create the following new file: `/view/frontend/layout/checkout_index_index.xml`. (For your checkout customization to be applied correctly, your custom [module](https://glossary.magento.com/module) should depend on the Magento_Checkout module.) +1. In this file, add the following: + + ```xml + + + + + + + ... + + + + + + ``` + +1. In the `/view/frontend/layout/checkout_index_index.xml` file, find the component that you need to customize. Copy the corresponding node and all parent nodes up to ``. There is no need to leave all the attributes and values of parent nodes, as you are not going to change them. + +1. Change the path to the component's `.js` file, template or any other property. + +Example: + +The Magento_Shipping module adds a component rendered as a link to the Shipping Policy info to the Shipping step: + +`/view/frontend/layout/checkout_index_index.xml` looks like following: + +```xml + + + + + + + + + + + + + + + + + + Magento_Shipping/js/view/checkout/shipping/shipping-policy + + + + + + + + + + + + + + + + + + +``` + +## Add the new component to the checkout page layout {#add} + +Any [UI component](https://glossary.magento.com/ui-component) is added in the `checkout_index_index.xml` similar to the way a [checkout step component is added]({{ page.baseurl }}/howdoi/checkout/checkout_new_step.html#checkout). + +Make sure that you declare a component so that it is rendered correctly by the parent component. If a parent component is a general UI component (referenced by the `uiComponent` alias), its child components are rendered without any conditions. But if a parent component is an extension of a general UI components, then children rendering might be restricted in certain way. For example a component can render only children from a certain `displayArea`. + +## Move a component + +To move any component on your checkout page, find the component (parent) where it needs to be placed, and paste your component as a child of the parent. + +The following example shows how to move the discount component to the order summary block, which will display on both shipping and billing steps. + +```xml + + + + + + + + + + + + + + Magento_SalesRule/js/view/payment/discount + + + 0 + Magento_SalesRule/js/view/payment/discount-messages + messages + + + + + + + + + + + + + + + +``` + +{:.bs-callout-info} +Remember to [disable](#disable) or [remove](#remove) the component from its original location, or they will conflict with each other. + +### Order Summary Result + +![Discount Component]({{ site.baseurl }}/common/images/ui_comps/discount_component.png) + +## Disable a component {#disable} + +To disable the component in your `checkout_index_index.xml` use the following instructions: + +```xml + + + true + + +``` + +## Remove a component {#remove} + +To keep a component from being rendered, create a layout processor. A layout processor consists of a class, implementing +the `\Magento\Checkout\Block\Checkout\LayoutProcessorInterface` interface, and thus a `LayoutProcessorInterface::process($jsLayout)` method. + +```php +\\Block\Checkout; + +use Magento\Checkout\Block\Checkout\LayoutProcessorInterface; + +class OurLayoutProcessor implements LayoutProcessorInterface +{ + /** + * @param array $jsLayout + * @return array + */ + public function process($jsLayout) + { + //%path_to_target_node% is the path to the component's node in checkout_index_index. + unset($jsLayout['components']['checkout']['children']['steps'][%path_to_target_node%]); + return $jsLayout; + } +} +``` + +Once created, add the layout processor through Dependency Injection (DI). + +```xml + + + + + + \\Block\Checkout\OurLayoutProcessor + + + + +``` + +To use this sample in your code, replace the `%path_to_target_node%` placeholder with real value. + +{:.bs-callout-info} +Disable vs remove a component: A disabled component is loaded but not rendered. If you remove a component, it is removed and not loaded. diff --git a/src/guides/v2.3/howdoi/checkout/checkout_edit_form.md b/src/guides/v2.3/howdoi/checkout/checkout_edit_form.md new file mode 100644 index 00000000000..abd62dced5e --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_edit_form.md @@ -0,0 +1,111 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add a custom template for a form field on Checkout page +subtitle: Customize Checkout +menu_order: 8 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how to replace the [HTML](https://glossary.magento.com/html) template for a form field on the [Checkout](https://glossary.magento.com/checkout) page. You might need to replace the template in order to add elements displayed with the field, change the [CSS](https://glossary.magento.com/css) class assigned to it, add attributes and so on. + +The forms used on the Checkout page are implemented using Knockout JS. + +To change the template of the form field, do the following: + +1. [Create a custom HTML template for knockout JS script that will render the form field](#template). +1. [Specify the new template in the Checkout page layout](#layout). +1. [Clear files after modification](#modify). + +## Prerequisites + +[Set Magento to the developer mode]({{ page.baseurl }}/config-guide/cli/config-cli-subcommands-mode.html) when performing all customizations and debugging. + +For the sake of compatibility, upgradability, and easy maintenance, do not edit the default Magento code. Instead, add your customizations in a separate [module](https://glossary.magento.com/module). For your checkout customization to be applied correctly, your custom module should [depend]({{ page.baseurl }}/extension-dev-guide/build/composer-integration.html) on the Magento_Checkout module. + +Do not use `Ui` for your custom module name, because `%Vendor%_Ui` notation, required when specifying paths, might cause issues. + +## Step 1: Implement the HTML template for the field {#template} + +Create a new `.html` template in the following directory: `/view/frontend/web/template/form/element` + +Example of a field template: + +```html + + + +image_de +``` + + {:.bs-callout-info} +The original templates of all form field types are located in the `app/code/Magento/Ui/view/base/web/templates/form/element` directory. + +## Step 2: Specify the new template in layout {#layout} + +In your custom module directory, create a new `/view/frontend/layout/checkout_index_index.xml` file. +In this file, add content similar to the following: + +```xml + + + + + + + + + + + + + + + + + + + + + + + %Vendor_Module%/form/element/%your_template% + + + + + + + + + + + + + + + + + + +``` + +## Step 3: Clear files after modification {#modify} + +If you modify your custom `.html` template after it was applied on the store pages, the changes will not apply until you do the following: + +1. Delete all files in the `pub/static/frontend` and `var/view_preprocessed` directories. +1. Reload the pages. diff --git a/src/guides/v2.3/howdoi/checkout/checkout_form.md b/src/guides/v2.3/howdoi/checkout/checkout_form.md new file mode 100644 index 00000000000..eeb90c6bb1f --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_form.md @@ -0,0 +1,289 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add a new input form to checkout +subtitle: Customize Checkout +menu_order: 9 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how to add a custom input form (implemented as a UI component) to the [Checkout](https://glossary.magento.com/checkout) page. + +![The input form with four fields]({{ site.baseurl }}/common/images/how_checkout_form.png) + +Most of the elements, including the default forms on the Checkout page are implemented as UI components. And we recommend your custom form to be a UI component, extending the default [Magento_Ui/js/form/form]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Ui/view/base/web/js/form/form.js) component. + +Magento provides the ability to add a custom form to any of the checkout steps: Shipping Information, Review and Payment Information, or custom. In order to add a custom form that is a UI component, take the following steps: + +1. [Create the JS implementation of the form UI component](#component). +1. [Create the knockout.js HTML template for rendering the form](#template). +1. [Declare the form in the checkout page layout](#layout). + +## Prerequisites + +[Set Magento to developer mode]({{ page.baseurl }}/config-guide/cli/config-cli-subcommands-mode.html) while you perform all customizations and debugging. + +For the sake of compatibility, upgradability, and easy maintenance, do not edit the default Magento code. Instead, add your customizations in a separate module. For your checkout customization to be applied correctly, your custom module should [depend]({{ page.baseurl }}/extension-dev-guide/build/composer-integration.html) on the `Magento_Checkout` module. + +Do not use `Ui` for your custom module name, because `%Vendor%_Ui` notation, required when specifying paths, might cause issues. + +## Step 1: Create the JS implementation of the form UI component {#component} + +In your `/view/frontend/web/js/view/` directory, create a `custom-checkout-form.js` file implementing the form. + +Example of extending the default form component: + +```js +/*global define*/ +define([ + 'Magento_Ui/js/form/form' +], function(Component) { + 'use strict'; + return Component.extend({ + initialize: function () { + this._super(); + // component initialization logic + return this; + }, + + /** + * Form submit handler + * + * This method can have any name. + */ + onSubmit: function() { + // trigger form validation + this.source.set('params.invalid', false); + this.source.trigger('customCheckoutForm.data.validate'); + + // verify that form data is valid + if (!this.source.get('params.invalid')) { + // data is retrieved from data provider by value of the customScope property + var formData = this.source.get('customCheckoutForm'); + // do something with form data + console.dir(formData); + } + } + }); +}); +``` + +## Step 2: Create the HTML template {#template} + +Add the `knockout.js` HTML template for the form component under the `/view/frontend/web/template` directory called `custom-checkout-form.html`. + +Example: + +```html +
    +
    +
    + + + + +
    + + +
    +
    +``` + +## Step 3: Declare the form in the checkout page layout {#layout} + +Certain default checkout templates declare regions where additional content can be inserted. You can add your custom form in any of these regions. These regions are provided with corresponding comments in the default Checkout page layout file `/view/frontend/layout/checkout_index_index.xml`. + +Also you locate the regions in the `.html` templates of the blocks used in this [layout](https://glossary.magento.com/layout) file. +For example, the shipping JS component (see `/view/frontend/web/template/shipping.html`) provides the `before-form` region and corresponding UI container. + +Any content added here is rendered before the Shipping Address form on the Shipping Information step. To add content to this region, create a `checkout_index_index.xml` layout update in the `/view/frontend/layout/`. + +It should be similar to the following: + +```xml + + + + + + + + + + + + + + + + + + + + VendorName_ModuleName/js/view/custom-checkout-form + checkoutProvider + + + VendorName_ModuleName/custom-checkout-form + + + + + + + + + + + + + + + + + + + + + +``` + +### Clear files after modification {#modify} + +If you modify your custom `.html` template after it was applied on the store pages, the changes will not apply until you do the following: + +1. Delete all files in the `pub/static/frontend` and `var/view_preprocessed` directories. +1. Reload the pages. + +### Static forms {#static_form} + +The term static refers to the forms where all the fields are already known/predefined and do not depend on any settings in the [Admin](https://glossary.magento.com/admin). Compare to [dynamic forms](#dynamic_form). + +The fields of static forms are not generated dynamically, so they can be defined in a layout. + +The following code sample shows the configuration of the `custom-checkout-form-container` form, defined in the previous step. It contains four fields: a text input, a select, a checkbox, and a date field. This form uses the checkout data provider (`checkoutProvider`) that was introduced in the `Magento_Checkout` module: + +```xml +... + + ... + + + + uiComponent + + custom-checkout-form-fields + + + Magento_Ui/js/form/element/abstract + + + customCheckoutForm + ui/form/field + ui/form/element/input + + checkoutProvider + customCheckoutForm.text_field + Text Field + 1 + + true + + + + Magento_Ui/js/form/element/boolean + + + customCheckoutForm + ui/form/field + ui/form/element/checkbox + + checkoutProvider + customCheckoutForm.checkbox_field + Checkbox Field + 3 + + + Magento_Ui/js/form/element/select + + + customCheckoutForm + ui/form/field + ui/form/element/select + + + + Please select value + + + + Value 1 + value_1 + + + Value 2 + value_2 + + + + value_2 + checkoutProvider + customCheckoutForm.select_field + Select Field + 2 + + + Magento_Ui/js/form/element/date + + + customCheckoutForm + ui/form/field + ui/form/element/date + + checkoutProvider + customCheckoutForm.date_field + Date Field + + true + + + + + + +... +``` + +### Dynamically defined forms {#dynamic_form} + +Dynamically defined, or dynamic, forms are the forms where the set or type of fields can change. For example, the fields displayed on the checkout form depend on the Admin settings: depending on the **Admin > Stores > Settings > Configuration > Customers > Customer Configuration > Name and Address Options**. + +For such forms, you must implement a [plugin]({{ page.baseurl }}/extension-dev-guide/plugins.html) for the `\Magento\Checkout\Block\Checkout\LayoutProcessor::process` method. +A plugin can add custom fields definitions to layout at runtime. The format of the field definition is the same as for fields defined in layout. + +For example: + +```php?start_inline=1 +$textField = [ + 'component' => 'Magento_Ui/js/form/element/abstract', + 'config' => [ + 'customScope' => 'customCheckoutForm', + 'template' => 'ui/form/field', + 'elementTmpl' => 'ui/form/element/input', + ], + 'provider' => 'checkoutProvider', + 'dataScope' => 'customCheckoutForm.text_field', + 'label' => 'Text Field', + 'sortOrder' => 1, + 'validation' => [ + 'required-entry' => true, + ], +]; +``` diff --git a/src/guides/v2.3/howdoi/checkout/checkout_new_field.md b/src/guides/v2.3/howdoi/checkout/checkout_new_field.md new file mode 100644 index 00000000000..4e5f0d4593b --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_new_field.md @@ -0,0 +1,230 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add a new field in address form +subtitle: Customize Checkout +menu_order: 9 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +You can add new fields to default [checkout](https://glossary.magento.com/checkout) forms, such as shipping address or billing address forms. To illustrate this ability, this topic describes adding a field to the shipping address form. + +To add your custom field to the checkout address form and access its value on the client side: + +1. [Add the field to layout](#add). +1. [Add a JS mixin to modify data submission](#mixin). +1. [Load your mixin](#load_mixin). +1. [Add the field to address model](#field). +1. [Access the value of the custom field on server side](#access). + +## Step 1: Add the field to layout {#add} + +Both shipping address and billing address forms are [generated dynamically]({{ page.baseurl }}/howdoi/checkout/checkout_form.html#dynamic_form). To modify their layouts, create a [plugin]({{ page.baseurl }}/extension-dev-guide/plugins.html) for the `\Magento\Checkout\Block\Checkout\LayoutProcessor::process` method and declare it in the `di.xml` file in your module. + +The following code snippet enumerates sample logic for adding a field named `Custom Attribute` to the shipping address form: + +```php?start_inline=1 +$customAttributeCode = 'custom_field'; +$customField = [ + 'component' => 'Magento_Ui/js/form/element/abstract', + 'config' => [ + // customScope is used to group elements within a single form (e.g. they can be validated separately) + 'customScope' => 'shippingAddress.custom_attributes', + 'customEntry' => null, + 'template' => 'ui/form/field', + 'elementTmpl' => 'ui/form/element/input', + 'tooltip' => [ + 'description' => 'this is what the field is for', + ], + ], + 'dataScope' => 'shippingAddress.custom_attributes' . '.' . $customAttributeCode, + 'label' => 'Custom Attribute', + 'provider' => 'checkoutProvider', + 'sortOrder' => 0, + 'validation' => [ + 'required-entry' => true + ], + 'options' => [], + 'filterBy' => null, + 'customEntry' => null, + 'visible' => true, + 'value' => '' // value field is used to set a default value of the attribute +]; + +$jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']['shippingAddress']['children']['shipping-address-fieldset']['children'][$customAttributeCode] = $customField; +``` + +Via the previous example, the field is added to the `customAttributes` property of `'Magento_Checkout/js/model/new-customer-address.js`, a JavaScript object that lists all predefined address attributes and matches the corresponding server-side interface `\Magento\Quote\Api\Data\AddressInterface`. + +The `customAttributes` property was designed to contain custom EAV address attributes and is related to the `\Magento\Quote\Model\Quote\Address\CustomAttributeListInterface::getAttributes` method. The sample code above will automatically handle local storage persistence on the [frontend](https://glossary.magento.com/frontend). + +Optionally, instead of adding a plugin, you can use a [dependency injection (DI)]({{ page.baseurl }}/extension-dev-guide/depend-inj.html). To use a DI, add the `LayoutProcessor`, which adds the custom field to the address form class, to the `/Block/Checkout/` directory. The class must implement the `\Magento\Checkout\Block\Checkout\LayoutProcessorInterface` interface. Use the code sample above as an example of the `\Magento\Checkout\Block\Checkout\LayoutProcessorInterface::process()` method implementation. + +To add your `LayoutProcessor` class the corresponding pool of processors, specify the following (where `%unique_name%` and `%path\to\your\LayoutProcessor%` must be replaced by your real values) in the `/etc/frontend/di.xml` file: + +```xml + + + + %path\to\your\LayoutProcessor% + + + +``` + +## Step 2: Add a JS mixin to modify data submission {#mixin} + +Add a JS [mixin](https://glossary.magento.com/mixin), to the [server side](https://glossary.magento.com/server-side), to change the behavior of the component responsible for the data submission. + +In your custom module, define a mixin as a separate AMD module that returns a callback function. Add the mixin file anywhere in the `/view/frontend/web` directory. There are no strict requirements for the mixin file naming. + +The following code sample is a sample mixin modifying the behavior of `Magento_Checkout/js/action/set-shipping-information`, the component responsible for data submission between shipping and billing checkout steps: + +```js +/*jshint browser:true jquery:true*/ +/*global alert*/ +define([ + 'jquery', + 'mage/utils/wrapper', + 'Magento_Checkout/js/model/quote' +], function ($, wrapper, quote) { + 'use strict'; + + return function (setShippingInformationAction) { + + return wrapper.wrap(setShippingInformationAction, function (originalAction) { + var shippingAddress = quote.shippingAddress(); + if (shippingAddress['extension_attributes'] === undefined) { + shippingAddress['extension_attributes'] = {}; + } + + var attribute = shippingAddress.customAttributes.find( + function (element) { + return element.attribute_code === 'custom_field'; + } + ); + + shippingAddress['extension_attributes']['custom_field'] = attribute.value; + // pass execution to original action ('Magento_Checkout/js/action/set-shipping-information') + return originalAction(); + }); + }; +}); +``` + +When adding a field to the billing address form, you must modify the behavior of the `Magento_Checkout/js/action/place-order` or `Magento_Checkout/js/action/set-payment-information` component, depending on when do you need the custom field valued to be passed to the server side. + +To see an example of a mixing that modifies one of these components, see the [place-order-mixin.js]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/CheckoutAgreements/view/frontend/web/js/model/place-order-mixin.js) in the Magento_CheckoutAgreements [module](https://glossary.magento.com/module). + +## Step 3: Load your mixin {#load_mixin} + +Tell Magento to load your mixin for the corresponding JS component by adding the `requirejs-config.js` to the `/view/frontend/` directory. + +The following code sample shows an example utilizing the sample mixin added earlier: + +```js +var config = { + config: { + mixins: { + 'Magento_Checkout/js/action/set-shipping-information': { + '/js/action/set-shipping-information-mixin': true + } + } + } +}; +``` + +## Step 4: Add field to address model {#field} + +To add the field to the address model on the server side, add the `extension_attributes.xml` file in the `/etc/` directory. + +The following code is an example of an `extension_attributes.xml` file: + +```xml + + + + + + + +``` + +Clear the `generated/code` directory when you run the `setup:di:compile` command. New getter and setter methods will be added in `generated/code/Magento/Quote/Api/Data/AddressExtension.php` file. + +## Step 5: Access the value of the custom field on server side {#access} + +If you completed all the steps described in the previous sections, Magento will generate the interface that includes your custom attribute and you can access your field value. + +You can set/get these attributes values by creating an instance of the `Magento/Quote/Api/Data/AddressInterface.php` interface. + +```php +_addressInformation = $addressInformation; + parent::__construct($context, $data); + } + + /** + * Get custom Shipping Charge + * + * @return String + */ + public function getShippingCharge() + { + $extAttributes = $this->_addressInformation->getExtensionAttributes(); + return $extAttributes->getCustomField(); //get custom attribute data. + } +} +``` + +## Step 6: Run CLI commands + +1. Compile the code with: + + ```bash + bin/magento setup:di:compile + ``` + +1. Next, deploy static content: + + ```bash + bin/magento setup:static-content:deploy + ``` + +1. Then clean the cache: + + ```bash + bin/magento cache:clean + ``` + +{:.ref-header} +Related topic + +[EAV and extension attributes]({{ page.baseurl }}/extension-dev-guide/attributes.html) diff --git a/src/guides/v2.3/howdoi/checkout/checkout_new_step.md b/src/guides/v2.3/howdoi/checkout/checkout_new_step.md new file mode 100644 index 00000000000..db8b552ca4d --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_new_step.md @@ -0,0 +1,235 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add a new checkout step +subtitle: Customize Checkout +menu_order: 1 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how to create the [frontend](https://glossary.magento.com/frontend) part of the component, implementing a checkout step, and how to add it to the checkout flow. + +The default Magento [Checkout](https://glossary.magento.com/checkout) consists of two steps: + +- Shipping Information +- Review and Payments Information + +You can add a custom checkout step, it should be implemented as a [UI component](https://glossary.magento.com/ui-component). For the sake of compatibility, upgradability and easy maintenance, do not edit the default Magento code, add your customizations in a separate [module](https://glossary.magento.com/module). + +1. [Create the view part of the checkout step component](#create-view). +1. [Add your step to the Checkout page layout](#checkout). +1. [Create mixins for payment and shipping steps (optional)](#create-mixin). + +## Step 1: Create the view part of the checkout step component {#create-view} + +To create the view part of the new checkout step: + +1. Add a module directory (not covered in this topic). See [Build your module]({{ page.baseurl }}/extension-dev-guide/build/build.html) for details). All custom files must be stored there. For your checkout customization to be applied correctly, your custom module should depend on the `Magento_Checkout` module. Do not use `Ui` for your custom module name, because `%Vendor%_Ui` notation, required when specifying paths, might cause issues. +1. [Create the `.js` file implementing the view model](#component). +1. [Create a `.html` template for the component](#html-template). + +### Add the JavaScript file implementing the new step {#component} + +A new checkout step must be implemented as UI component. That is, its [JavaScript](https://glossary.magento.com/javascript) implementation must be a JavaScript module. + +The file must be stored under the `/view/frontend/web/js/view` directory. + +{:.bs-callout-info} +`` notation stands for the path to your module directory from the root directory. Usually it will be one of the following: `app/code//` or `vendor//module--`. For more details see [Conventional notations for paths to modules and themes]({{ page.baseurl }}/frontend-dev-guide/conventions.html) + +A sample `my-step-view.js` with comments follows: + +```js +define([ + 'ko', + 'uiComponent', + 'underscore', + 'Magento_Checkout/js/model/step-navigator' +], function (ko, Component, _, stepNavigator) { + 'use strict'; + + /** + * mystep - is the name of the component's .html template, + * _ - is the name of your module directory. + */ + return Component.extend({ + defaults: { + template: '_/mystep' + }, + + // add here your logic to display step, + isVisible: ko.observable(true), + + /** + * @returns {*} + */ + initialize: function () { + this._super(); + + // register your step + stepNavigator.registerStep( + // step code will be used as step content id in the component template + 'step_code', + // step alias + null, + // step title value + 'Step Title', + // observable property with logic when display step or hide step + this.isVisible, + + _.bind(this.navigate, this), + + /** + * sort order value + * 'sort order value' < 10: step displays before shipping step; + * 10 < 'sort order value' < 20 : step displays between shipping and payment step + * 'sort order value' > 20 : step displays after payment step + */ + 15 + ); + + return this; + }, + + /** + * The navigate() method is responsible for navigation between checkout steps + * during checkout. You can add custom logic, for example some conditions + * for switching to your custom step + * When the user navigates to the custom step via url anchor or back button we_must show step manually here + */ + navigate: function () { + this.isVisible(true); + }, + + /** + * @returns void + */ + navigateToNextStep: function () { + stepNavigator.next(); + } + }); +}); +``` + +### Add the .html template {#html-template} + +In the module directory, add the `.html` template for the component. It must be located under the `/view/frontend/web/template` directory. + +A sample `mystep.html` follows: + +```html + +
  • +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
  • +``` + +## Step 2: Add your step to the Checkout page layout {#checkout} + +For the new step to be displayed on the page, you need to declare it in the Checkout page layout, which is defined in `checkout_index_index.xml`. + +So you need to add an [extending]({{ page.baseurl }}/frontend-dev-guide/layouts/layout-extend.html) `checkout_index_index.xml` layout file in the following location: `/view/frontend/layout/checkout_index_index.xml` + +A sample `checkout_index_index.xml` follows: + +```xml + + + + + + + + + + + + + %Vendor%_%Module%/js/view/my-step-view + + + + 2 + + + + + + + + + + + + + + +``` + +## Step 3: Create mixins for payment and shipping steps (optional) {#create-mixin} + +If your new step is the first step, you have to create mixins for the payment and shipping steps. Otherwise, two steps will be activated on the loading of the checkout. + +Create a mixin as follows: + +1. Create a `Vendor/Module/view/base/requirejs-config.js` file with these contents; + + ```js + var config = { + 'config': { + 'mixins': { + 'Magento_Checkout/js/view/shipping': { + 'Vendor_Module/js/view/shipping-payment-mixin': true + }, + 'Magento_Checkout/js/view/payment': { + 'Vendor_Module/js/view/shipping-payment-mixin': true + } + } + } + } + ``` + +1. Create the mixin. We'll use the same mixin for both payment and shipping: + + ```js + define([ + 'ko' + ], function (ko) { + 'use strict'; + + var mixin = { + + initialize: function () { + // set visible to be initially false to have your step show first + this.visible = ko.observable(false); + this._super(); + + return this; + } + }; + + return function (target) { + return target.extend(mixin); + }; + }); + ``` + +{:.bs-callout-info} +For your changes to be applied, you might need to [clean layout cache]({{ page.baseurl }}/config-guide/cli/config-cli-subcommands-cache.html ) and [static view file cache]({{ page.baseurl }}/frontend-dev-guide/cache_for_frontdevs.html#clean_static_cache). For more info on mixins, see [JS Mixins]({{ page.baseurl }}/javascript-dev-guide/javascript/js_mixins.html). diff --git a/src/guides/v2.3/howdoi/checkout/checkout_order.md b/src/guides/v2.3/howdoi/checkout/checkout_order.md new file mode 100644 index 00000000000..0a022f723df --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_order.md @@ -0,0 +1,129 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add custom validations before order placement +subtitle: Customize Checkout +menu_order: 4 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how to add custom validations to be performed before the order is placed during [checkout](https://glossary.magento.com/checkout). Namely, the validations which are performed after a shopper clicks the **Place Order** button. Writing the validation logic itself is not covered in this topic. + +To add custom validations before the order placement action, you must do the following: + +1. [Create the validator](#validator). +1. [Add validator to the validators pool](#pool). +1. [Declare the validation in the checkout layout](#layout). + +## Step 1: Create the validator {#validator} + +For the sake of compatibility, upgradability and easy maintenance, do not edit the default Magento code, add your customizations in a separate module. For your checkout customization to be applied correctly, your custom module should [depend]({{ page.baseurl }}/extension-dev-guide/build/composer-integration.html) on the `Magento_Checkout` module. Do not use `Ui` for your custom module name, because `%Vendor%_Ui` notation, required when specifying paths, might cause issues. + +In your custom module directory, create a `.js` file implementing the validator. It should be located under `/view/frontend/web/js/model` directory. + +Following is a sample of the validator `.js` file. It must necessarily implement the `validate()` method: + +```js +define( + ['mage/translate', 'Magento_Ui/js/model/messageList'], + function ($t, messageList) { + 'use strict'; + return { + validate: function () { + const isValid = false; //Put your validation logic here + + if (!isValid) { + messageList.addErrorMessage({ message: $t('a possible failure message ... ') }); + } + + return isValid; + } + } + } +); +``` + +## Step 2: Add validator to the validators pool {#pool} + +Your custom validator must be added to the pool of "additional validators". To do this, in the `/view/frontend/web/js/view` directory create a new `.js` file with the following content: + +```js +define( + [ + 'uiComponent', + 'Magento_Checkout/js/model/payment/additional-validators', + '/js/model/your-validator' + ], + function (Component, additionalValidators, yourValidator) { + 'use strict'; + additionalValidators.registerValidator(yourValidator); + return Component.extend({}); + } +); +``` + +## Step 3: Declare the validation in the checkout layout {#layout} + +In your custom module directory, create a new `/view/frontend/layout/checkout_index_index.xml` file. + +In this file, add the following: + +```xml + + + + + + + + + + + + + + + + + + + %your_module_dir%/js/view/%your-validation% + + + + + + + + + + + + + + + + + + +``` + +## Step 4: Deploy static content and clean cache + +{:.bs-callout-info} +These commands are for production mode. They are not necessary when in developer mode. + +1. Deploy static content: + + ```bash + bin/magento setup:static-content:deploy + ``` + +1. Clean cache: + + ```bash + bin/magento cache:clean + ``` diff --git a/src/guides/v2.3/howdoi/checkout/checkout_overview.md b/src/guides/v2.3/howdoi/checkout/checkout_overview.md new file mode 100644 index 00000000000..da468936397 --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_overview.md @@ -0,0 +1,42 @@ +--- +layout: tutorial +group: how-do-i +subgroup: checkout +title: Customize Checkout +menu_title: Initial Tasks +menu_node: +level3_subgroup: checkout-tutorial +menu_order: 0 +functional_areas: + - Checkout +--- + +Magento [checkout](https://glossary.magento.com/checkout) is implemented using the [UI components]({{ page.baseurl }}/ui_comp_guide/bk-ui_comps.html). +Out of the box, the checkout consists of two steps: + +- Shipping Information +- Review and Payment Information + +The checkout totals and the corresponding side-bar are only displayed after the first step is completed. + +The only [exception](https://glossary.magento.com/exception) is checkout of virtual and/or downloadable products: if there are only these types of products in the shopping cart, checkout is automatically transformed to one-step procedure, because shipping information is not required. + + {:.bs-callout-info} +For the sake of compatibility, upgradability, and easy maintenance, do not edit the default Magento code. Add your customizations in a custom [module](https://glossary.magento.com/module). + +## List of available customizations + +You can customize the default checkout in multiple ways. This tutorial includes the following customizations: + +- [Add a new checkout step]({{ page.baseurl }}/howdoi/checkout/checkout_new_step.html) +- [Customize the view of an existing step]({{ page.baseurl }}/howdoi/checkout/checkout_customize.html) +- [Add a custom payment method to checkout]({{ page.baseurl }}/howdoi/checkout/checkout_payment.html) +- [Add custom validations before order placement]({{ page.baseurl }}/howdoi/checkout/checkout_order.html) +- [Add custom shipping carrier]({{ page.baseurl }}/howdoi/checkout/checkout-add-custom-carrier.html) +- [Add custom shipping carrier validations]({{ page.baseurl }}/howdoi/checkout/checkout_carrier.html) +- [Add custom input mask for ZIP code]({{ page.baseurl }}/howdoi/checkout/checkout_zip.html) +- [Add a custom template for a form field on Checkout page]({{ page.baseurl }}/howdoi/checkout/checkout_edit_form.html) +- [Add a new input form to checkout]({{ page.baseurl }}/howdoi/checkout/checkout_form.html) +- [Add a new field in address form]({{ page.baseurl }}/howdoi/checkout/checkout_new_field.html) +- [Add custom shipping address renderer]({{ page.baseurl }}/howdoi/checkout/checkout_address.html) +- [Add a custom field for an offline payment method]({{ page.baseurl }}/howdoi/checkout/checkout_payment_new_field.html) diff --git a/src/guides/v2.3/howdoi/checkout/checkout_payment.md b/src/guides/v2.3/howdoi/checkout/checkout_payment.md new file mode 100644 index 00000000000..90dc3d58b9f --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_payment.md @@ -0,0 +1,333 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add a custom payment method to checkout +subtitle: Customize Checkout +menu_order: 3 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +Out of the box, Magento [checkout](https://glossary.magento.com/checkout) consists of two steps: + +- Shipping Information +- Review and Payment Information + +On the Review and Payment Information step the enabled payment methods are rendered. This topic describes how to add your custom [payment method](https://glossary.magento.com/payment-method) to this list. + +To implement a payment method rendering in checkout, you need to take the following steps: + +1. [Create the `.js` file implementing the component (payment method renderer).](#create) +1. [Create the `.js` component registering the payment method renderer.](#register) +1. [Create a template for the payment method renderer.](#template) +1. [Declare the new payment in the checkout page layout.](#layout) + +## Step 1: Create the .js component file {#create} + +Your payment method renderer must be implemented as a [UI component](https://glossary.magento.com/ui-component). For the sake of compatibility, upgradability and easy maintenance, do not edit the default Magento code, add your customizations in a separate module. For your checkout customization to be applied correctly, your custom module should depend on the `Magento_Checkout` module. Module dependencies are specified in the [module's `composer.json`]({{ page.baseurl }}/extension-dev-guide/build/composer-integration.html). + +Do not use `Ui` for your custom module name, because `%Vendor%_Ui` notation, required when specifying paths, might cause issues. + +In your custom module directory create the component's `.js` file (payment method renderer). It must be located under the `/view/frontend/web/js/view/` directory. For example in the Magento modules, the payment methods renderers are stored in the `/view/frontend/web/js/view/payment/method-renderer/` directory. + +Usually, your component will extend the default payment method component (default payment method renderer) implemented in the `/view/frontend/web/js/view/payment/default.js` file. The following table contains the list of the `default` component's methods. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MethodDescription
    getCode():stringReturns the code of the payment method
    getData():objectReturns an object with the payment data to be sent to the server on selecting a payment method and/or an extension (on pressing Continue button). It must contain data according to \Magento\Quote\Api\Data\PaymentInterface. All the payment information except the method code and purchase order number is passed in the additional_data field.
    placeOrder():boolPlaces an order if all validations passed.
    selectPaymentMethod():boolAdds information about the payment method selected by the user to the Quote JS object.
    isChecked():stringReturns the code of the selected payment method.
    isRadioButtonVisible():bool Returns true if only one payment method is available.
    getTitle():stringReturns the payment method title.
    validate():boolUsed in the placeOrder() method. So you can override validate() in your module, and this validation will be performed in the scope of placeOrder().
    getBillingAddressFormName():stringGets the unique billing address name.
    disposeSubscriptions()Terminates the object's subscription.
    + +The general view of the payment method renderer is the following: + +```js +define( + [ + 'Magento_Checkout/js/view/payment/default' + ], + function (Component) { + 'use strict'; + return Component.extend({ + defaults: { + template: '%path to template%' + }, + // add required logic here + }); + } +); +``` + +If your payment method requires credit cards information, you might use the Magento renderer implementing a credit card form: [`/view/frontend/web/js/view/payment/cc-form.js`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Payment/view/frontend/web/js/view/payment/cc-form.js). It also extends the default payment renderer, but has the following own methods: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MethodDescription
    getData():object Returns an object with the payment data to be sent to the server on selecting a payment method and/or an extension (on pressing Continue button). It must contain data according to \Magento\Quote\Api\Data\PaymentInterface. All the payment information except the method code and purchase order number is passed in the additional_data field. Adds credit card data (type, issue date, number, CVV).
    getCcAvailableTypes():arrayReturns the list of available credit card types.
    getIcons():boolReturns links to the images for available credit card types.
    getCcMonths():objectRetrieves the month of the credit card expiration date.
    getCcYears():objectRetrieves the year of the credit card expiration date.
    hasVerification():boolA flag that shows if the credit card CVV number is required for this payment.
    hasSsCardType():boolReturns true if the Solo and Switch (Maestro) card types are available.
    getCvvImageUrl():stringRetrieves the CVV tooltip image URL.
    getCvvImageHtml():stringRetrieves the CVV tooltip image HTML.
    getSsStartYears():objectSolo or Switch (Maestro) card start year.
    + +### Access the system config data {#system-config-data} + +Your payment method might need to get data that cannot be defined in [layout](https://glossary.magento.com/layout) configuration, JS components or templates directly, for example, data from the Magento system config. +This configuration is stored in the `window.checkoutConfig` variable that is defined in root checkout template. + +In order to get access to the system configuration, your payment method or a group of payment methods has to implement the [`\Magento\Checkout\Model\ConfigProviderInterface`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Checkout/Model/ConfigProviderInterface.php) interface, and the class implementing it must be injected to the composite config provider via DI [frontend](https://glossary.magento.com/frontend) configuration. The following code samples illustrate this. + +This is a sample `.php` file implementing `\Magento\Checkout\Model\ConfigProviderInterface`: + +```php?start_inline=1 +class MyCustomPaymentConfigProvider implements \Magento\Checkout\Model\ConfigProviderInterface +{ +... + public function getConfig() + { + return [ + // 'key' => 'value' pairs of configuration + ]; + } +... +} +``` + +Here is the associated sample DI configuration file of a custom module `/etc/frontend/di.xml`: + +```xml +... + + + + ... + MyCustomPaymentConfigProvider + ... + + +... + +``` + +### Add other payment-related features {#payment-features} + +You can also add payment-related features (like reward points, gift registry, an so on) to the Review and Payment Information checkout step. They must be implemented as UI components as well, and can be displayed before or after the list of payment methods. This is configured in the [checkout page layout file correspondingly](#layout). + +## Step 2: Create the .js component that registers the renderer {#register} + +In your custom module directory create the `.js` UI component that registers the payment method renderer in the renderers list. It must be located under the `/view/frontend/web/js/view/` directory. For example in the Magento modules, the payment methods renderers are stored in the `/view/frontend/web/js/view/payment/` directory. + +The file content must be similar to the following: + +```js +define( + [ + 'uiComponent', + 'Magento_Checkout/js/model/payment/renderer-list' + ], + function ( + Component, + rendererList + ) { + 'use strict'; + rendererList.push( + { + type: '%payment_method_code%', + component: '%js_renderer_component%' + }, + // other payment method renderers if required + ); + /** Add view logic here if needed */ + return Component.extend({}); + } +); +``` + +If your [module](https://glossary.magento.com/module) adds several payment methods, you can register all payment methods renderers in one file. + +## Step 3: Create the template for the payment method component {#template} + +In your custom module directory create a new `/view/frontend/web/template/.html` file. The template can use [Knockout JS](https://knockoutjs.com/) syntax. You can find a sample `.html` template in any module implementing payment methods, for example the Magento_Authorizenet module. + +The template for rendering the Authorize.Net payment method in checkout is [`/view/frontend/web/template/payment/authorizenet-directpost.html`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Authorizenet/view/frontend/web/template/payment/authorizenet-directpost.html). + +## Step 4: Declare the payment method in layout {#layout} + +In your custom module directory, create a new `/view/frontend/layout/checkout_index_index.xml` file. In this file, add the following: + +```xml + + + + + + + + + + + + uiComponent + + + + + + uiComponent + beforeMethods + + + %path/to/your/feature_js_component% + + + + + + + + + + %component_that_registers_payment_renderer% + + + + + true + + + + + + + + + + uiComponent + afterMethods + + + %path/to/your/feature_js_component% + + + + + + + + + + + + + + + + + + +``` + +## Step 5:Run CLI Commands {#compile-code-deploy-contents-and-clean-the-cache} + +These steps are required in production mode only, not while in development mode. + +1. Compile the code: + + ```bash + bin/magento setup:di:compile + ``` + +1. Deploy the static contents: + + ```bash + bin/magento setup:static-content:deploy + ``` + +1. Clean the cache: + + ```bash + bin/magento cache:clean + ``` + +For an illustration of `checkout_index_index.xml` where a new payment method is declared, view [app/code/Magento/Authorizenet/view/frontend/layout/checkout_index_index.xml]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Authorizenet/view/frontend/layout/checkout_index_index.xml) diff --git a/src/guides/v2.3/howdoi/checkout/checkout_payment_new_field.md b/src/guides/v2.3/howdoi/checkout/checkout_payment_new_field.md new file mode 100644 index 00000000000..9de6a58c3a6 --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_payment_new_field.md @@ -0,0 +1,362 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add a custom field for an offline payment method +contributor_name: Ziffity +contributor_link: https://www.ziffity.com/ +subtitle: Customize Checkout +menu_order: 101 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how to add a custom field to an offline payment method in the payment step of the checkout. The custom field allows the buyer to enter a comment about a purchase order. + +## Prerequisites + +{:.bs-callout-info} +The `Purchase Order` payment method must be enabled in the storefront for this task. Ensure this payment method is enabled by navigating to **Stores** > **Settings** > **Configuration** > **Sales** > **Payment Methods** > **Other Payment Methods** > **Purchase Order** in the Admin. + +You must perform following steps to add a custom field to an offline payment method: + +1. [Create a new module](#create-module). +1. [Add a `db_schema.xml` file](#add-db-schema). +1. [Add a `requirejs` file to the module](#add-require-js). +1. [Override the vendor files](#override-vendor-files). +1. [Add an Observer](#add-observer). +1. [Compile and deploy the module](#compile-deploy). +1. [Verify that the module works](#verify-implementation). + +Let’s go through each step. + +## Step 1: Create a new module {#create-module} + +[Create a new module]({{ site.baseurl }}/videos/fundamentals/create-a-new-module/) named `Learning/CustomField` and register it. + +## Step 2 Add a `db_schema.xml` file {#add-db-schema} + +Add the `paymentpocomment` column in the `quote_payment` and `sales_order_payment` tables using the [declarative schema]({{page.baseurl}}/extension-dev-guide/declarative-schema/db-schema.html) method. + +Create the `app/code/Learning/CustomField/etc/db_schema.xml` and define the declarative schema as follows: + +```xml + + + +
    + + +
    +
    +``` + +## Step 3: Add a requirejs file to the module {#add-require-js} + +Create the `app/code/Learning/CustomField/view/frontend/requirejs-config.js` file and add the following code: + +```js + var config = { + map: { + '*': { + 'Magento_OfflinePayments/js/view/payment/offline-payments':'Learning_CustomField/js/view/payment/offline-payments', + } + } +} +``` + +## Step 4: Override the vendor files {#override-vendor-files} + +We must override the behavior of the following files to display the custom field: + +- [Magento_OfflinePayments/view/frontend/web/js/view/payment/offline-payments.js](#offline-payment) +- [Magento_OfflinePayments/view/frontend/web/js/view/payment/method-renderer/purchaseorder-method.js](#purchaseorder-method) +- [Magento_OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html](#purchaseorder-form) + +### Override the `offline-payments.js` {#offline-payment} + +Override the `Magento_OfflinePayments/view/frontend/web/js/view/payment/offline-payments.js` file to change the renderer of the Purchase Order payment method. + +Create the `app/code/Learning/CustomField/view/frontend/web/js/view/payment/offline-payments.js` file and add the following code: + +```js +define( + [ + 'uiComponent', + 'Magento_Checkout/js/model/payment/renderer-list' + ], + function ( + Component, + rendererList + ) { + 'use strict'; + rendererList.push( + { + type: 'checkmo', + component: 'Magento_OfflinePayments/js/view/payment/method-renderer/checkmo-method' + }, + { + type: 'banktransfer', + component: 'Magento_OfflinePayments/js/view/payment/method-renderer/banktransfer-method' + }, + { + type: 'cashondelivery', + component: 'Magento_OfflinePayments/js/view/payment/method-renderer/cashondelivery-method' + }, + { + type: 'purchaseorder', + component: 'Learning_CustomField/js/view/payment/method-renderer/purchaseorder-method' + } + ); + /** Add view logic here if needed */ + return Component.extend({}); + } +); +``` + +### Override the `purchaseorder-method.js` {#purchaseorder-method} + +It is also necessary to override the `Magento_OfflinePayments/view/frontend/web/js/view/payment/method-renderer/purchaseorder-method.js` file. + +The `template` path value used in this file must be altered to use the custom template. Also, the logic to get the `additional_data` is implemented in this file. + +Create the `app/code/Learning/CustomField/view/frontend/web/js/view/payment/method-renderer/purchaseorder-method.js` file and add the following code: + +```js +define([ + 'Magento_Checkout/js/view/payment/default', + 'jquery', + 'mage/validation' +], function (Component, $) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Learning_CustomField/payment/purchaseorder-form', + purchaseOrderNumber: '' + }, + + /** @inheritdoc */ + initObservable: function () { + this._super() + .observe('purchaseOrderNumber'); + + return this; + }, + + /** + * @return {Object} + */ + getData: function () { + return { + method: this.item.method, + 'po_number': this.purchaseOrderNumber(), + 'additional_data': { + 'po_number': $('#po_number').val(), + 'paymentpocomment': $('#purchaseorder_paymentpocomment').val(), + } + }; + }, + + /** + * @return {jQuery} + */ + validate: function () { + var form = 'form[data-role=purchaseorder-form]'; + + return $(form).validation() && $(form).validation('isValid'); + } + }); +}); +``` + +### Override the `purchaseorder-form.html` {#purchaseorder-form} + +We must override the `Magento_OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html` template file to add the custom input field (**Purchase Order Comment**). + +Create the `app/code/Learning/CustomField/view/frontend/web/template/payment/purchaseorder-form.html` file and add the following code: + +{% collapsible Show code %} + +```html +
    +
    + + +
    + +
    + + + +
    + + + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    + +
    +
    +
    +
    +``` +{% endcollapsible %} + +## Step 5: Add an Observer {#add-observer} + +Create an Observer file to save the custom field data to the order. For the Observer file an `events.xml` file is required to call the observer for a particular event. For this example, the `checkout_onepage_controller_success_action` event is used. + +Create the `app/code/Learning/CustomField/etc/frontend/events.xml` file and add the following code: + +```xml + + + + + + + +``` + +Then create the `app/code/Learning/CustomField/Observer/Frontend/Sales/OrderPaymentSaveBefore.php` file. + +{% collapsible Show code %} + +```php +order = $order; + $this->quoteRepository = $quoteRepository; + $this->logger = $logger; + $this->_serialize = $serialize; + } + /** + * Execute observer + * + * @param \Magento\Framework\Event\Observer $observer + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + $orderids = $observer->getEvent()->getOrderIds(); + if(!$orderids){ + foreach ($orderids as $orderid) { + $order = $this->_order->load($orderid); + $method = $order->getPayment()->getMethod(); + if($method == 'purchaseorder') { + $quote_id = $order->getQuoteId(); + $quote = $this->quoteRepository->get($quote_id); + $paymentQuote = $quote->getPayment(); + $paymentOrder = $order->getPayment(); + $paymentOrder->setData('paymentpocomment',$paymentQuote->getPaymentpocomment()); + $paymentOrder->save(); + } + } + } + } +} +``` +{% endcollapsible %} + +## Step 6: Compile and deploy the module {#compile-deploy} + +Run the following sequence of commands to compile and deploy your custom module. + +1. Enable the new module. + + ```bash + bin/magento module:enable Learning_CustomField + ``` + +1. Install the new module. + + ```bash + bin/magento setup:upgrade + ``` + +1. Compile the code. + + ```bash + bin/magento setup:di:compile + ``` + +1. Deploy the static files. + + ```bash + bin/magento setup:static-content:deploy + ``` + +## Step 7: Verify that the module works {#verify-implementation} + +Use the following steps to verify your changes work as expected. + +1. Go to the storefront as a guest user and add a product to the cart. + +1. Go to the checkout page and select the **Purchase Order** payment. + +1. Verify that the **Purchase Order Comment** field is visible. + + ![Custom field in checkout page]({{ site.baseurl }}/common/images/custom_field_payment.png) + +1. Fill the purchase order comment field in the checkout and place an order. + +1. Verify that the entered value is stored in the `paymentpocomment` column of the `sales_order_payment` table. diff --git a/src/guides/v2.3/howdoi/checkout/checkout_shipping_methods.md b/src/guides/v2.3/howdoi/checkout/checkout_shipping_methods.md new file mode 100644 index 00000000000..f8a4fd2e640 --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_shipping_methods.md @@ -0,0 +1,137 @@ +--- +layout: tutorial +group: how-do-i +subgroup: checkout +title: Customize the list of shipping methods +subtitle: Customize Checkout +menu_order: 100 +level3_subgroup: checkout-tutorial +--- + +This topic describes how to customize list of shipping methods displayed on the checkout page. + +Let's consider a case where you need to add a collapsible text field with description for each shipping method in this list. To achieve this, you need to take the following steps: + +1. [Create a new template for the shipping method item](#method-item). +1. [Create a new template for the shipping method list](#method-list). +1. [Override the shipping step configuration](#shipping). + +## Step 1: Create new template for shipping method item {#method-item} + +In your custom module directory, create a new file: `/view/frontend/web/template/custom-method-item-template.html`. In this file, add the following code. + +It is copied from the `/view/frontend/web/template/shipping-address/shipping-method-item.html` template, with the following modifications: + +* A `` element added to contain the shipping method description +* A column with trigger elements that provide the collapse/expand functionality added +* The entire sample wrapped in `` to provide the general collapsible context for rows + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## Step 2: Create new template for shipping methods list {#method-list} + +In your custom module directory, create a new file: `/view/frontend/web/template/custom-method-list-template.html`. In this file, add the following code. It uses the code from the `app/code/Magento/Checkout/view/frontend/web/template/shipping-address/shipping-method-list.html` template, with the following modifications: + +* A column for triggers added in `` +* `tbody` moved to the item template for collapsible context + +```html +
    + + + +
    + + + + + +
    +
    +``` + +## Step 3: Override shipping step configuration {#shipping} + +In your custom module directory, create a new file: `/view/frontend/layout/checkout_index_index.xml`. In this file, add the following code. + +It overrides the `shippingMethodListTemplate` and `shippingMethodItemTemplate` properties of `/view/frontend/web/js/view/shipping.js`: + +```html + + + + + + + + + + + + + + + Vendor_Checkout/custom-method-item-template + Vendor_Checkout/custom-method-list-template + + + + + + + + + + + + + + +``` diff --git a/src/guides/v2.3/howdoi/checkout/checkout_zip.md b/src/guides/v2.3/howdoi/checkout/checkout_zip.md new file mode 100644 index 00000000000..e053683879e --- /dev/null +++ b/src/guides/v2.3/howdoi/checkout/checkout_zip.md @@ -0,0 +1,117 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add custom input mask for ZIP code +subtitle: Customize Checkout +menu_order: 7 +level3_subgroup: checkout-tutorial +functional_areas: + - Checkout +--- + +This topic describes how a developer can add custom input masks. + +When a shopper specifies the country and ZIP code in the shipping address during [checkout](https://glossary.magento.com/checkout) or in the shopping cart, Magento checks if the format of the entered code is valid for the specified country. This validation is implemented using the input masks for the ZIP code field. In Magento, these input masks are regular expressions which define which format is allowed. + +In Magento the input masks for the **ZIP code** field are specified in the `/etc/zip_codes.xml`. Input masks are specified per country, and are entered in the form of regular expressions. +The syntax of defined by the [zip_code.xsd]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Directory/etc/zip_codes.xsd) scheme. + +The following table defines the `zip` node attributes: + + Attribute name | Required | Description +--- | --- | --- +`countryCode` | Yes | The country code (Alpha-2 format) for which the zip is defined + +```xml + + + +``` + +The following table defines the `code` node attributes: + + Attribute name | Required | Description +--- | --- | --- +`id` | Yes | A random unique name within the same list. +`example` | Yes | An example of the allowed pattern. +`active` | No | Defines if this zip pattern is active or not. + +You can define several zip `code` patterns for the same country, by passing a list of `codes`. +```xml + + ^[0-9]{5}$ + ^[a-zA-z]{2}[0-9]{4}$ + +``` + +For the sake of compatibility, upgradability, and easy maintenance, do not edit the default Magento code. Add your customizations in a separate, custom module. For your ZIP code input mask customization to be applied correctly, your custom module should depend on the `Magento_Directory` module. Do not use `Ui` for your custom module name, because `%Vendor%_Ui` notation, required when specifying paths, might cause issues. + +## Add custom ZIP code input masks {#add} + +To add custom ZIP code input masks or change the default ones, create a new `zip_codes.xml` in the `/etc` directory. + +The content of the file should be similar to the following sample: + +```xml + + + + + + + + + ^[0-9]{5}\-[0-9]{4}$ + ^[0-9]{5}$ + + + +``` + +## Modify default values in existing mask {#modify} + +To change (override the default value) the existing mask: + +1. Open `zip_codes.xml`. +1. Copy in the related nodes. +1. Change the regular expression defining the mask and the value of `example` correspondingly. + +Example of changing the default input mask: + +In the default `/etc/zip_codes.xml` the following mask is set for France: + +```xml + + + +... + + + ^[0-9]{5}$ + + +... + +``` + +To change this mask, add the following code in your `zip_codes.xml`: + +```xml + + + +... + + + + ^[a-zA-Z]{1}[0-9]{3}$ + + +... + +``` + +## Remove a mask {#remove} + +To remove a mask, in your `zip_codes.xml` add the corresponding node and set `active` attribute of `` to `false`. diff --git a/src/guides/v2.3/howdoi/checkout/images/checkout-add-custom-carrier-01.png b/src/guides/v2.3/howdoi/checkout/images/checkout-add-custom-carrier-01.png new file mode 100644 index 00000000000..1e84d502a0e Binary files /dev/null and b/src/guides/v2.3/howdoi/checkout/images/checkout-add-custom-carrier-01.png differ diff --git a/src/guides/v2.3/howdoi/checkout/images/checkout-add-custom-carrier-02.png b/src/guides/v2.3/howdoi/checkout/images/checkout-add-custom-carrier-02.png new file mode 100644 index 00000000000..4302ddaaa1a Binary files /dev/null and b/src/guides/v2.3/howdoi/checkout/images/checkout-add-custom-carrier-02.png differ diff --git a/src/guides/v2.3/howdoi/custom-attributes/introduction.md b/src/guides/v2.3/howdoi/custom-attributes/introduction.md new file mode 100644 index 00000000000..cfbffb05a40 --- /dev/null +++ b/src/guides/v2.3/howdoi/custom-attributes/introduction.md @@ -0,0 +1,13 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Custom attributes +menu_node: +level3_subgroup: custom-attributes +menu_order: 1 +contributor_name: Adarsh Manickam +contributor_link: https://github.com/drpayyne +--- + +The tutorials under this section guide Magento developers on setting up and configuring custom attributes. diff --git a/src/guides/v2.3/howdoi/custom-attributes/text-field.md b/src/guides/v2.3/howdoi/custom-attributes/text-field.md new file mode 100644 index 00000000000..020d8af7c00 --- /dev/null +++ b/src/guides/v2.3/howdoi/custom-attributes/text-field.md @@ -0,0 +1,348 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Add a custom text field attribute +menu_node: +level3_subgroup: custom-attributes +menu_order: 2 +contributor_name: Adarsh Manickam +contributor_link: https://github.com/drpayyne +--- + +## Overview + +This tutorial describes how a developer can create a custom text field attribute for the Customer entity using code. This will reflect in both the [Customer Grid]({{ site.user_guide_url }}/customers/customer-account-manage.html) and the [Customer Form]({{ site.user_guide_url }}/customers/customer-account-update.html) in the Admin. + +This Customer attribute will be used to save and view the customer's ID in an external system, as an example. It will be created as an EAV attribute in a data patch. The EAV model allows a developer to add custom functionality to the Magento entities without modifying the core databases and schemas. Data patches are run just once, so this code will create the custom attribute and will never run again, which could cause issues. + +## Code + +### Create the data patch class + +Create a data patch class called `ExternalId` under the `\ExampleCorp\Customer\Setup\Patch\Data` namespace. This makes Magento execute the data patch automatically when `bin/magento setup:upgrade` is run. All data patches must implement the `\Magento\Framework\Setup\Patch\DataPatchInterface` interface. + +```php +moduleDataSetup = $moduleDataSetup; + $this->customerSetup = $customerSetupFactory->create(['setup' => $moduleDataSetup]); + $this->attributeResource = $attributeResource; + $this->logger = $logger; + } + ``` + +### Implement the apply method + +There are five steps in developing a data patch. All the steps below are written inside the `apply` method. + +1. Starting and ending the setup execution. This turns off foreign key checks and sets the SQL mode. + + ```php + $this->moduleDataSetup->getConnection()->startSetup(); + + /* + Attribute creation code must be run between these two lines + to ensure that the attribute is created smoothly. + */ + + $this->moduleDataSetup->getConnection()->endSetup(); + ``` + +1. Add the text field customer attribute with the required settings. + + The third parameter for `addAttribute` is an array of settings required to configure the attribute. Passing an empty array uses all the default values for each possible setting. To keep the code to a minimum, just declare the settings needing to be overridden and the rest of the settings will be used from the Magento defaults. The settings overrides can be done as described below. + + {:.bs-callout-info} + For creating a simple text field, it is not necessary to override the settings for `backend` (database field type) and `input` (frontend HTML input type), as they default to `varchar` and `text` respectively. + + {:.bs-callout-info} + The `\Magento\Customer\Api\CustomerMetadataInterface` interface contains constants like the customer entity's code and the default attribute set code, which can be referenced. + + ```php + $this->customerSetup->addAttribute( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, // entity type code + 'externalcorp_external_id', // unique attribute code + [ + 'label' => 'External ID', + 'required' => 0, + 'position' => 200, + 'system' => 0, + 'user_defined' => 1, + 'is_used_in_grid' => 1, + 'is_visible_in_grid' => 1, + 'is_filterable_in_grid' => 1, + 'is_searchable_in_grid' => 1, + ] + ); + ``` + + | Setting Key | Description | + | --- | --- | + | `label` | `External ID` - Label for displaying the attribute value | + | `required` | `0` - Attribute will be an optional field in the customer form | + | `position` | `200` - Sort order in the customer form | + | `system` | `0` - Not a system-defined attribute | + | `user_defined` | `1` - A user-defined attribute | + | `is_used_in_grid` | `1` - Ready for use in the customer grid | + | `is_visible_in_grid` | `1` - Visible in the customer grid | + | `is_filterable_in_grid` | `1` - Filterable in the customer grid | + | `is_searchable_in_grid` | `1` - Searchable in the customer grid | + +1. Add attribute to an attribute set and group. + + {:.bs-callout-info} + There is only one attribute set and group for the customer entity. The default attribute set ID is a constant defined the `CustomerMetadataInterface` interface and setting the attribute group ID to null makes Magento use the default attribute group ID for the customer entity. + + ```php + $this->customerSetup->addAttributeToSet( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, // entity type code + CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, // attribute set ID + null, // attribute group ID + 'externalcorp_external_id' // unique attribute code + ); + ``` + +1. Make the attribute visible in the customer form. + + ```php + // Get the newly created attribute's model + $attribute = $this->customerSetup->getEavConfig() + ->getAttribute(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, 'externalcorp_external_id'); + + // Make attribute visible in Admin customer form + $attribute->setData('used_in_forms', [ + 'adminhtml_customer' + ]); + + // Save modified attribute model using its resource model + $this->attributeResource->save($attribute); + ``` + +1. Gracefully handle exceptions. + + ```php + try { + // All the code inside the apply method goes into the try block. + } catch (Exception $exception) { + $this->logger->err($exception->getMessage()); + } + ``` + +### Implement rest of the interface + +This data patch does not have any other patch as a dependency, and this data patch was not renamed earlier, so both `getDependencies` and `getAliases` can return an empty array. + +```php +public static function getDependencies(): array +{ + return []; +} + +public function getAliases(): array +{ + return []; +} +``` + +### Execute the data patch + +Run `bin/magento setup:upgrade` from the Magento root to execute the newly added data patch. + +- The attribute is created in the customer form under the _Account Information_ section. + +![Custom attribute in the customer form]({{ site.baseurl }}/common/images/custom-attributes/customer-text-form.png){:width="600px"} + +- The attribute is displayed in the customer grid. + +![Custom attribute in the customer grid]({{ site.baseurl }}/common/images/custom-attributes/customer-text-grid.png){:width="600px"} + +### Code reference + +```php +moduleDataSetup = $moduleDataSetup; + $this->customerSetup = $customerSetupFactory->create(['setup' => $moduleDataSetup]); + $this->attributeResource = $attributeResource; + $this->logger = $logger; + } + + /** + * Get array of patches that have to be executed prior to this. + * + * Example of implementation: + * + * [ + * \Vendor_Name\Module_Name\Setup\Patch\Patch1::class, + * \Vendor_Name\Module_Name\Setup\Patch\Patch2::class + * ] + * + * @return string[] + */ + public static function getDependencies(): array + { + return []; + } + + /** + * Get aliases (previous names) for the patch. + * + * @return string[] + */ + public function getAliases(): array + { + return []; + } + + /** + * Run code inside patch + */ + public function apply() + { + // Start setup + $this->moduleDataSetup->getConnection()->startSetup(); + + try { + // Add customer attribute with settings + $this->customerSetup->addAttribute( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + 'externalcorp_external_id', + [ + 'label' => 'External ID', + 'required' => 0, + 'position' => 100, + 'system' => 0, + 'user_defined' => 1, + 'is_used_in_grid' => 1, + 'is_visible_in_grid' => 1, + 'is_filterable_in_grid' => 1, + 'is_searchable_in_grid' => 1, + ] + ); + + // Add attribute to default attribute set and group + $this->customerSetup->addAttributeToSet( + CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, + CustomerMetadataInterface::ATTRIBUTE_SET_ID_CUSTOMER, + null, + 'externalcorp_external_id' + ); + + // Get the newly created attribute's model + $attribute = $this->customerSetup->getEavConfig() + ->getAttribute(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, 'externalcorp_external_id'); + + // Make attribute visible in Admin customer form + $attribute->setData('used_in_forms', [ + 'adminhtml_customer' + ]); + + // Save attribute using its resource model + $this->attributeResource->save($attribute); + } catch (Exception $e) { + $this->logger->err($e->getMessage()); + } + + // End setup + $this->moduleDataSetup->getConnection()->endSetup(); + } +} +``` diff --git a/src/guides/v2.3/howdoi/customize-form-configuration.md b/src/guides/v2.3/howdoi/customize-form-configuration.md new file mode 100644 index 00000000000..75da160a50e --- /dev/null +++ b/src/guides/v2.3/howdoi/customize-form-configuration.md @@ -0,0 +1,83 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Customize the form configuration +subtitle: Customize Product Creation Form +menu_node: +level3_subgroup: product-creation-form +menu_order: 2 +--- + +Customizing the form config file (that is, declarative customization) is preferable for changes like introducing new fields, field sets and modals. + +To customize the product creation form, take the following steps: + +1. In your custom module, add an empty `product_form.xml` in the `/view/adminhtml/ui_component/` directory. + +1. In this file, add content similar to the following: + +```xml +
    + +... + +
    + + + %field set label as displayed in UI% + %order for displaying% + + + +
    + + + %Nested fieldset Label as displayed in UI% + true + + + + + + %value% + %value% +.... + + + +
    +
    +... +
    +``` + +## Add new elements {#add-elements} + +By default, the new elements (fields, field sets, modals, grids) which you add in the form configuration file, are displayed on the form whatever product is created; that is, for all [product types](https://glossary.magento.com/product-types). + +In the [modifier class described further]({{page.baseurl}}/howdoi/customize-modifier-class.html), you can set the conditions for displaying certain elements for certain product types. + +## Customize existing fields and field sets {#customize} + +Your `product_form.xml` is merged with the same files from the other modules. So there is no need to copy their content, you only need to define changes. Even if you want to customize the existing entities, you only have to mention those options, the values of which are customized. + +To customize an existing entity, declare only those options, the values of which are customized, do not copy its entire configuration. + +To delete an existing field, or field set, in your `product_form.xml` use the following construction: + +```xml +... +
    + + true + +
    +... +``` + +For reference, view the product form configuration files of the Magento modules: + +* `/view/adminhtml/ui_component/product_form.xml` +* `/view/adminhtml/ui_component/product_form.xml` +* `/view/adminhtml/ui_component/product_form.xml` diff --git a/src/guides/v2.3/howdoi/customize-modifier-class.md b/src/guides/v2.3/howdoi/customize-modifier-class.md new file mode 100644 index 00000000000..3b32010f130 --- /dev/null +++ b/src/guides/v2.3/howdoi/customize-modifier-class.md @@ -0,0 +1,127 @@ +--- +layout: tutorial +group: how-do-i +subgroup: +title: Customize using a modifier class +subtitle: Customize Product Creation Form +menu_node: +level3_subgroup: product-creation-form +menu_order: 3 +--- + +[Modifier classes]({{ page.baseurl }}/ui_comp_guide/concepts/ui_comp_modifier_concept.html) should be used when static declaration is not applicable. For example, in cases when additional data should be loaded from database. Also, modifier is a place where you add validations to display only certain fields for certain product types. + +In the run time, the form structure set in the modifier is merged with the configuration that comes from the `product_form.xml` configuration. + +The `Magento\Catalog\Ui\DataProvider\Product\Form\ProductDataProvider` data provider class is responsible for data and metadata preparation for the product form. The pool of modifiers `Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool` (virtual type) is injected to this data provider using the `__construct()` method. The pool's preference is defined in `di.xml`. + +To add your custom modifier, you need to do the following: + +1. [Add the modifier code.](#modifier) +1. [Add it to the modifiers' pool in `di.xml`](#pool) + +## Add your modifier {#modifier} + +In your custom module directory, add the modifier class that implements the `Magento\UI\DataProvider\Modifier\ModifierInterface` interface or extends the `Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\AbstractModifier`class. In your modifier, the `modifyData()` and the `modifyMeta()` methods must be implemented. + +In the modifier class, you can add UI elements using the same structure as in the [XML](https://glossary.magento.com/xml) configuration. + +For example: + +```php + [ + 'data' => [ + 'config' => [ + 'label' => __('Label For Fieldset'), + 'sortOrder' => 50, + 'collapsible' => true, + 'componentType' => Fieldset::NAME + ] + ] + ], + 'children' => [ + 'test_field_name' => [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'formElement' => 'select', + 'componentType' => Field::NAME, + 'options' => [ + ['value' => 'test_value_1', 'label' => 'Test Value 1'], + ['value' => 'test_value_2', 'label' => 'Test Value 2'], + ['value' => 'test_value_3', 'label' => 'Test Value 3'], + ], + 'visible' => 1, + 'required' => 1, + 'label' => __('Label For Element') + ] + ] + ] + ] + ] + ]; + + return $meta; + } + + /** + * @inheritDoc + */ + public function modifyData(array $data) + { + return $data; + } +} +``` + +You can create nested structures of elements by adding them to the `children` key of any element. + +## Add modifier to the pool {#pool} + +In `/etc/adminhtml/di.xml` define your modifier as a dependency for `Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Pool`. + +The following is an example of such a definition: + +`app/code/Magento/CatalogInventory/etc/adminhtml/di.xml`: + +```xml + + + + + Magento\CatalogInventory\Ui\DataProvider\Product\Form\Modifier\AdvancedInventory + 20 + + + + +``` + +The `sortOrder` parameter defines the order of invocation for your `modifyData()` and `modifyMeta()` methods among other these methods of other modifiers in the pool. If a modifier is first in a pool, its `modifyData()` and `modifyMeta()` are invoked with empty arguments. + +To access product model within your modifier, it's recommended to use an instance of `Magento\Catalog\Model\Locator\LocatorInterface`. + +For reference, view the modifier classes in the Magento modules, for example: + +- [`Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php) +- [`Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AttributeSet.php) +- [`Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php) +- [`Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts`]({{ site.mage2bloburl }}/{{ page.guide_version }}/app/code/Magento/ConfigurableProduct/Ui/DataProvider/Product/Form/Modifier/Data/AssociatedProducts.php) + +For reference about setting conditions for displaying certain elements for certain product types, view `/Ui/DataProvider/Product/Form/Modifier/Eav.php#L476`. diff --git a/src/guides/v2.3/howdoi/customize_product.md b/src/guides/v2.3/howdoi/customize_product.md new file mode 100644 index 00000000000..286c2c884b2 --- /dev/null +++ b/src/guides/v2.3/howdoi/customize_product.md @@ -0,0 +1,38 @@ +--- +layout: tutorial +group: how-do-i +subgroup: product-create-page +title: Customize product creation form +menu_title: Initial Tasks +menu_node: +level3_subgroup: product-creation-form +menu_order: 1 +--- + +This tutorial describes how developers can customize the product creation form used on the product creation and product edit pages in [Admin](https://glossary.magento.com/admin). The product creation form is implemented using the [form UI component]({{ page.baseurl }}/ui_comp_guide/components/ui-form.html). + +Product attributes and attribute sets available in the form, can be customized and added under **STORES** > **Attributes** in the Admin. But you can also customize the form view and behavior in code. The following sections describe what files define the form and how they can be customized in your [module](https://glossary.magento.com/module). + +## Prerequisites {#prereqs} + +[Set Magento to developer mode]({{ page.baseurl }}/config-guide/cli/config-cli-subcommands-mode.html) while you perform all customizations and debugging. + +For the sake of compatibility, upgradability, and easy maintenance, do not edit the default Magento code. Instead, add your customizations in a separate module. + +## List of customization methods + +This tutorial includes the following customizations: + +* [Customize the form configuration]({{ page.baseurl }}/howdoi/customize-form-configuration.html) +* [Customize using a modifier class]({{ page.baseurl }}/howdoi/customize-modifier-class.html) + +{:.ref-header} +Related topics + +* [Form UI component]({{ page.baseurl }}/ui_comp_guide/components/ui-form.html) +* [About PHP modifiers in UI components]({{ page.baseurl }}/ui_comp_guide/concepts/ui_comp_modifier_concept.html) +* [Dependency injection]({{ page.baseurl }}/extension-dev-guide/depend-inj.html) + +The following image is an illustration of the default view of the form on the **New Product** page: + +![The product creation page in Admin]({{ site.baseurl }}/common/images/product_pmg.png) diff --git a/src/guides/v2.3/howdoi/php/php_clear-dirs.md b/src/guides/v2.3/howdoi/php/php_clear-dirs.md new file mode 100644 index 00000000000..92bfc49d17c --- /dev/null +++ b/src/guides/v2.3/howdoi/php/php_clear-dirs.md @@ -0,0 +1,71 @@ +--- +group: php-developer-guide +subgroup: 99_Module Development +title: Clear directories during development +menu_title: Clear directories during development +menu_node: +menu_order: 200 +--- + +#### Contents + +* [Overview of directory clearing](#howdoi-clear-over) +* [What directories to clear](#howdoi-clear-what) +* [How to clear the directories](#howdoi-clear-how) + +## Overview of directory clearing {#howdoi-clear-over} + +While you're developing Magento components (modules, themes, and language packages), your rapidly changing environment requires you to periodically clear certain directories and caches. Otherwise, your code runs with exceptions and won't function properly. + +This topic provides guidelines on what directories to clear and when to clear them. +All directories discussed in this topic are default locations. It's possible to customize these locations but doing so is beyond the scope of this topic. + +When you're developing Magento components (modules, themes, and language packages), the following directories contain temporary or generated files you can clear periodically: + +Directory | Description +--- | --- +`generated/code` | Contains [generated code][] +`generated/metadata`| Contains the compiled dependency injection configuration for all modules. +`pub/static`| Contains `js` and `html` files for each store view. +`var/cache` | All cacheable objects _except the page cache_. This directory is empty if you use a third-party cache storage such as Redis. +`var/composer_home` | Home directory for Setup Wizard artifacts. Do not touch this directory unless you are an experienced developer familiar with the Magento plug-in system. +`var/page_cache` | Cached pages from the full page cache mechanism. This directory is empty if you use a third-party HTTP accelerator such as Varnish. +`var/view_preprocessed` | Contains minified templates and compiled LESS (meaning LESS, CSS, and HTML). + +## What directories to clear {#howdoi-clear-what} + +The following table provides guidelines on what you should clear and when. + +Action | Directories to clear +--- | --- +Change a class if there is a plugin related to it | `generated/metadata`, `generated/code` +A change that results in generated factories or proxies | `generated/metadata`, `generated/code` +A change in `di.xml` | `generated/metadata`, `generated/code` (also need to run the code compiler again) +Add, remove, enable, or disable modules | `generated/metadata`, `generated/code`, `var/cache`, `var/page_cache` +Add or edit a layout or theme | `var/view_preprocessed`, `var/cache`, `var/page_cache` +Change LESS or templates | `var/view_preprocessed`, `var/cache`, `var/page_cache`, `pub/static` +Change `*.js` or `*.html` files | `pub/static` +Add or edit a CMS page, cacheable block, or use the Admin to change the configuration |`var/cache`, `var/page_cache` + +## How to clear the directories {#howdoi-clear-how} + +To only clear directories and not perform other actions, log in to the Magento server as the file system owner and clear directories using a command like the following: + +```bash +rm -r /generated/*/* +``` + +You can also use the following command-line tools clear some directories for you. These commands perform other tasks as well; consult the linked documentation for more details. + +| Tool name | Brief description | What it clears | +| --- | --- | --- | +| [`magento setup:upgrade`][]| Update Magento database schema and data | `generated/metadata` and `generated/code` | +| [`magento setup:di:compile`][]|Generates code | `generated/code` prior to compiling | +| [`magento deploy:mode:set {mode}`][]|Switch between `developer` and `production` mode | `generated/metadata`, `generated/code`, `var/view_preprocessed`| +| [`magento cache:clean {type}`][]|Clears the cache | `var/cache` and `var/page_cache`| + +[`magento setup:upgrade`]: {{ page.baseurl }}/install-gde/install/cli/install-cli-subcommands-db-upgr.html +[`magento setup:di:compile`]: {{ page.baseurl }}/config-guide/cli/config-cli-subcommands-compiler.html +[`magento deploy:mode:set {mode}`]: {{ page.baseurl }}/config-guide/cli/config-cli-subcommands-mode.html +[`magento cache:clean {type}`]: {{ page.baseurl }}/config-guide/cli/config-cli-subcommands-cache.html +[generated code]: {{ page.baseurl }}/extension-dev-guide/code-generation.html diff --git a/src/guides/v2.3/index.html b/src/guides/v2.3/index.html new file mode 100644 index 00000000000..895220e4409 --- /dev/null +++ b/src/guides/v2.3/index.html @@ -0,0 +1,70 @@ +--- +layout: home +--- + +
    +
    {{site.data.var.ee}} 2.3 reached end of support in September 2022.
    + +
    diff --git a/src/guides/v2.3/install-gde/bk-install-guide.md b/src/guides/v2.3/install-gde/bk-install-guide.md new file mode 100644 index 00000000000..f86bdfe211a --- /dev/null +++ b/src/guides/v2.3/install-gde/bk-install-guide.md @@ -0,0 +1,69 @@ +--- +title: Get the software +landing-page: Installation Guide +functional_areas: + - Install + - System + - Setup +redirect_from: + - /guides/v2.3/install-gde/continue-to-install.html +--- + +You are among the 240,000 merchants worldwide who put their trust in our eCommerce software. We have gathered some information to help you get started with Magento and with your Magento installation. + +We have some resources here to help get you started using the eCommerce platform of the future---Magento 2. + +## How to get the Magento software {#install-get-software} + +Check the availability of new features and releases, and learn how you can obtain them, on our [Magento product availability page]({{ site.baseurl }}/release/availability.html). + +Consult the following table for getting started with installing {{site.data.var.ce}} or {{site.data.var.ee}}. + + + + + + + + + + + + + + + + + + + + + + +
    User needsDescriptionHigh-level installation and upgrade stepsGet started link

    Integrator, packager

    Wants full control over all components installed, has access to the Magento server, highly technical, might repackage {{site.data.var.ce}} with other components.

    +
    1. Creates a Composer project that contains the list of components to use.
    2. +
    3. Uses Composer to update package dependencies; uses composer create-project to get the Magento metapackage.
    4. +
    5. Installs the Magento software using the command line.
    6. +
    7. Upgrades the Magento application and extensions using the command line.

    Get the metapackage

    Contributing developer

    Contributes to the Magento codebase, files bugs, and customizes the Magento software. Highly technical, has their own Magento development server, understands Composer and GitHub.

    +

    You cannot use Magento in a production environment.

    +

    You must upgrade using Composer and Git commands.

    1. Clones the Magento 2 GitHub repository.
    2. +
    3. Uses Composer to update package dependencies.
    4. +
    5. Installs the Magento software using command line.
    6. +
    7. Upgrades the Magento software using Composer and Git commands.
    8. +
    9. Customizes code under the app/code directory.

    Clone the Magento repository

    + +## Useful information + +Use the links on the left side of the page to navigate topics in each part of the installation. + +## Required server permissions + +UNIX systems require `root` privileges to install and configure software like a web server, PHP, and so on. If you need to install this software, make sure you have `root` access. + +You should *not* install the Magento software in the web server docroot as the `root` user because the web server might not be able to interact with those files. + +You'll also need `root` privileges to create the [file system owner] and add that owner to the web server's group. You'll use the [file system owner](https://glossary.magento.com/magento-file-system-owner) to run any commands from the command line and to set up the Magento cron job, which schedules tasks for you. + + + +[file system owner]: {{ page.baseurl }}/install-gde/prereq/file-sys-perms-over.html diff --git a/src/guides/v2.3/install-gde/composer.md b/src/guides/v2.3/install-gde/composer.md new file mode 100644 index 00000000000..185ff84fa7b --- /dev/null +++ b/src/guides/v2.3/install-gde/composer.md @@ -0,0 +1,163 @@ +--- +title: Quick start install +functional_areas: + - Install + - System + - Setup +redirect_from: + - guides/v2.3/install-gde/prereq/zip_install.html +--- + +{% include install/composer-overview.md %} + +## Prerequisites + +Before you continue, you must do the following: + +- Set up a server that meets our [system requirements][]. +- Complete all [prerequisite tasks][]. +- Create and switch to the [file system owner](#instgde-cli-before). +- [Install Composer][]. +- Obtain [authentication keys][] for the Magento code repository. + +## Log in as file system owner {#instgde-cli-before} + +Learn about ownership, permissions, and the file system owner in our [Overview of ownership and permissions topic]({{ page.baseurl }}/install-gde/prereq/file-sys-perms-over.html). + +To switch to the file system owner: +{% include install/first-steps-cli.md %} +In addition to the command arguments discussed here, see [Common arguments]({{ page.baseurl }}/install-gde/install/cli/install-cli-subcommands.html#instgde-cli-subcommands-common). +## Get the metapackage + +To get the Magento metapackage: + +1. Log in to your Magento server as, or switch to, the [file system owner][]. +1. Change to the web server docroot directory or a directory that you have configured as a virtual host docroot. +1. Create a new Composer project using the {{site.data.var.ce}} or {{site.data.var.ee}} metapackage. + + **{{site.data.var.ce}}** + + ```bash + composer create-project --repository-url=https://repo.magento.com/ magento/project-community-edition + ``` + + **{{site.data.var.ee}}** + + ```bash + composer create-project --repository-url=https://repo.magento.com/ magento/project-enterprise-edition + ``` + + When prompted, enter your Magento authentication keys. Public and private keys are created and configured in your [Commerce Marketplace][]. + + If you encounter errors, such as `Could not find package...` or `...no matching package found`, make sure there are no typos in your command. If you still encounter errors, you may not be authorized to download {{site.data.var.ee}}. Contact [Magento support](https://magento.com/support) for help. + + See [troubleshooting][] for help with more errors. + + {% include install/pre-release.md %} + +### Example - Minor release + +Minor releases contain new features, quality fixes, and security fixes. Use Composer to specify a minor release. For example, to specify the {{site.data.var.ee}} 2.4.0 metapackage: + +```bash +composer create-project --repository-url=https://repo.magento.com/ magento/project-enterprise-edition=2.4.0 +``` + +### Example - Quality patch + +Quality patches primarily contain functional _and_ security fixes. However, they can also sometimes contain new, backward-compatible features. Use Composer to download a quality patch. For example, to specify the {{site.data.var.ee}} 2.3.4 metapackage: + +```bash +composer create-project --repository-url=https://repo.magento.com/ magento/project-enterprise-edition=2.3.4 +``` + +### Example - Security patch + +Security patches contain security fixes only. They are designed to make the upgrade process faster and easier. + +Security patches use the Composer naming convention `2.3.2-px`. Use Composer to specify a patch. For example, to download the {{site.data.var.ee}} 2.3.2-p1 metapackage: + +```bash +composer create-project --repository-url=https://repo.magento.com/ magento/project-enterprise-edition=2.3.2-p1 +``` + +## Set file permissions + +You must set read-write permissions for the web server group before you install the Magento software. This is necessary so that the command line can write files to the Magento file system. + +```terminal +cd /var/www/html/ +find var generated vendor pub/static pub/media app/etc -type f -exec chmod g+w {} + +find var generated vendor pub/static pub/media app/etc -type d -exec chmod g+ws {} + +chown -R :www-data . # Ubuntu +chmod u+x bin/magento +``` + +## Install Magento + +You must use the command line to install Magento. + +This example assumes that the Magento install directory is named `magento2ee`, the `db-host` is on the same machine (`localhost`), and that the `db-name`, `db-user`, and `db-password` are all `magento`: + +```bash +bin/magento setup:install \ +--base-url=http://localhost/magento2ee \ +--db-host=localhost \ +--db-name=magento \ +--db-user=magento \ +--db-password=magento \ +--admin-firstname=admin \ +--admin-lastname=admin \ +--admin-email=admin@admin.com \ +--admin-user=admin \ +--admin-password=admin123 \ +--language=en_US \ +--currency=USD \ +--timezone=America/Chicago \ +--use-rewrites=1 +``` + +{:.bs-callout-tip} +You can customize the Admin URI with the `--backend-frontname` option. However, we recommend omitting this option and allowing the installation command to automatically generate a random URI. A random URI is harder for hackers or malicious software to exploit. The URI displays in your console when installation is complete. + +{:.bs-callout-tip} +For a full description of the CLI install options, refer to [Install the Magento software from the command line][]. + +## Command summary {#instgde-cli-summary} +{% include install/cli_help-commands.md %} + +The following table summarizes the available commands. Commands are shown in summary form only; for more information about a command, click the link in the Command column. + +|Command|Description|Prerequisites| +|--- |--- |--- | +|`magento setup:install`|Installs the Magento software|None| +|`magento setup:uninstall`|Removes the Magento software.|Magento software installed| +|`magento setup:upgrade`|Updates the Magento software.|Deployment configuration| +|`magento maintenance:{enable/disable}`|Enables or disables maintenance mode (in maintenance mode, only exempt IP addresses can access the Admin or storefront).|Magento software installed| +|`magento setup:config:set`|Creates or updates the deployment configuration.|None| +|`magento module:{enable/disable}`|Enable or disable modules.|None| +|`magento setup:store-config:set`|Sets storefront-related options, such as base URL, language, timezone, and so on.|Deployment configuration +Database (simplest way is to use magento setup:upgrade)| +|`magento setup:db-schema:upgrade`|Updates the Magento database schema.|Deployment configuration| +|`magento setup:db-data:upgrade`|Updates the Magento database data.|Deployment configuration| +|`magento setup:db:status`|Checks if the database is up-to-date with the code.|Deployment configuration| +|`magento admin:user:create`|Creates a Magento administrator.|All of the following:

    Deployment configuration

    Enable at minimum the Magento_User and Magento_Authorization modules

    Database (simplest way is to use magento setup:upgrade)| +|`magento list`|Lists all available commands.|None| +|`magento help`|Provides help for the specified command.|None| + +### Common arguments {#instgde-cli-subcommands-common} +{% include install/cli_common-commands.md %} + +{:.bs-callout-info} +Hooray! You've completed the quick install. Need more advanced help? Check out our [Advanced install]({{ page.baseurl }}/install-gde/install/cli/install-cli.html) guide. + + +[Commerce Marketplace]: https://marketplace.magento.com/customer/accessKeys/ +[Modify docroot for security]: {{page.baseurl}}/install-gde/tutorials/change-docroot-to-pub.html +[Install the Magento software from the command line]: {{page.baseurl}}/install-gde/install/cli/install-cli.html +[troubleshooting]: https://support.magento.com/hc/en-us/articles/360033818091 +[file system owner]: {{page.baseurl}}/install-gde/prereq/file-sys-perms-over.html +[authentication keys]: {{page.baseurl}}/install-gde/prereq/connect-auth.html +[Install Composer]: https://getcomposer.org/download/ +[system requirements]: {{ page.baseurl }}/install-gde/system-requirements.html +[prerequisite tasks]: {{ page.baseurl }}/install-gde/prereq/prereq-overview.html diff --git a/src/guides/v2.3/install-gde/continue-to-verify_cli.md b/src/guides/v2.3/install-gde/continue-to-verify_cli.md new file mode 100644 index 00000000000..189fb1656da --- /dev/null +++ b/src/guides/v2.3/install-gde/continue-to-verify_cli.md @@ -0,0 +1,31 @@ +--- +title: Post-installation +functional_areas: + - Install + - System + - Setup +redirect_from: /guides/v2.3/install-gde/continue-to-verify.html +--- + +## Now that you've finished your installation + +We suggest the following: + +* [Verify the installation]({{ page.baseurl }}/install-gde/install/verify.html) + +## Other options + +You can also do any of the following: + +### Install optional sample data + +If you haven't already installed optional sample data, you can [install it now]({{ page.baseurl }}/install-gde/install/sample-data.html). + +### Manage and upgrade components + +* [Component management (install, uninstall, enable, disable, update)](https://experienceleague.adobe.com/docs/commerce-operations/upgrade-guide/modules/manage.html) +* [System upgrade (upgrade the Magento software)](https://experienceleague.adobe.com/docs/commerce-operations/upgrade-guide/implementation/perform-upgrade.html) + +### Configure + +[Configure the Magento application]({{ page.baseurl }}/install-gde/install/post-install-config.html) diff --git a/src/guides/v2.3/install-gde/contrib-git.md b/src/guides/v2.3/install-gde/contrib-git.md new file mode 100644 index 00000000000..d409c9cc146 --- /dev/null +++ b/src/guides/v2.3/install-gde/contrib-git.md @@ -0,0 +1,27 @@ +--- +title: Contributor install +functional_areas: + - Install + - System + - Setup +--- + +If you are a code or documentation contributor, this install guide is for you! Use Composer to install Magento, then switch over to a released version and update any installation dependencies. + +{:.bs-callout-tip} +Are you a non-contributor? Check out our [Quick start install]({{ page.baseurl }}/install-gde/composer.html) guide. Do you have more advanced install problems to solve? Check out our [Advanced install]({{ page.baseurl }}/install-gde/install/cli/install-cli.html) guide. + +## Intended audience {#integrator-aud} + +The audience for this topic is anyone who contributes to the {{site.data.var.ce}} codebase. +You should be highly technical, understand [Composer](https://glossary.magento.com/composer) and Git commands, and be able to upgrade the Magento system software and extensions using those commands. If that isn't you, go back and [choose another starting point]({{ page.baseurl }}/install-gde/bk-install-guide.html). + +{:.bs-callout-warning} +If you clone the Magento 2 GitHub repository, you _cannot_ use the Magento software in a production environment. +You cannot have a live store that accepts orders and so on. + +## Prerequisites + +{% include install/prereq.md %} + +{% include install/composer-overview.md %} \ No newline at end of file diff --git a/src/guides/v2.3/install-gde/install-flow-diagram.md b/src/guides/v2.3/install-gde/install-flow-diagram.md new file mode 100644 index 00000000000..bfcdeeaaf58 --- /dev/null +++ b/src/guides/v2.3/install-gde/install-flow-diagram.md @@ -0,0 +1,11 @@ +--- +title: Installation flow diagram +functional_areas: + - Install + - System + - Setup +--- + +The following diagram provides a high-level overview of installing the Magento software: + +{% include install/flow-diagram.md %} \ No newline at end of file diff --git a/src/guides/v2.3/install-gde/install/cli/dev_add-update.md b/src/guides/v2.3/install-gde/install/cli/dev_add-update.md new file mode 100644 index 00000000000..153b7a4f6bf --- /dev/null +++ b/src/guides/v2.3/install-gde/install/cli/dev_add-update.md @@ -0,0 +1,80 @@ +--- +subgroup: 99_contrib +title: Add or update components +menu_title: Add or update components +menu_order: 5 +menu_node: +functional_areas: + - Install + - System + - Setup +--- + +A contributing developer updates components by specifying components and their versions in Magento's `composer.json`. + +To update components if you're *not* a contributing developer, see [Updating the Magento application and components](https://experienceleague.adobe.com/docs/commerce-operations/upgrade-guide/overview.html). + +You can either add a `require` section to `composer.json` or you can use the `composer require` command as follows: + +1. Log in to the Magento server, or switch to, the [file system owner](https://glossary.magento.com/magento-file-system-owner). +1. Change to the directory to which you cloned the Magento application. For example, + + ```bash + cd /var/www/magento2 + ``` + +You have the following options: + +### Get available module versions + +Command usage: + +```bash +composer show --all / +``` + +For example, + +```bash +composer show --all example/module +``` + +### Use the `composer require` command to install + +Command usage: + +```bash +composer require /: +``` + +For example: + +```bash +composer require example/module:1.0.0 +``` + +Wait while [Composer](https://glossary.magento.com/composer) updates dependencies and installs the component. + +### Add a `require` section to `composer.json` + +Open `composer.json` in a text editor. + +Add a `require` section like the following: + +```json +"require": { + "/": "", + "/": "" +} +``` + +Save your changes to `composer.json`, exit the text editor, and enter `composer update` + +{:.ref-header} +Related topics + +If you have issues, see [Composer troubleshooting](https://getcomposer.org/doc/articles/troubleshooting.md). + + + +If you have issues, see [Composer troubleshooting](https://getcomposer.org/doc/articles/troubleshooting.md). diff --git a/src/guides/v2.3/install-gde/install/cli/dev_downgrade.md b/src/guides/v2.3/install-gde/install/cli/dev_downgrade.md new file mode 100644 index 00000000000..12482b5f366 --- /dev/null +++ b/src/guides/v2.3/install-gde/install/cli/dev_downgrade.md @@ -0,0 +1,91 @@ +--- +title: Change to a released version +functional_areas: + - Install + - System + - Setup +redirect_to: https://developer.adobe.com/commerce/contributor/guides/install/change-version/ +status: migrated +--- + +This topic discusses how a contributing developer can change versions of the Magento software after cloning the `develop` branch. This might be necessary to perform some tasks that require a specific Magento version other than `develop`. + +The `develop` branch is the default branch, which means you get it by default when you clone the Magento 2 GitHub repository. For some tasks, such as data migration from Magento 1.x to Magento 2.x, you must switch to a [release tag](https://github.com/magento/magento2/tags). + +You have the following options: + +* *(Easier)*. If you have not done any customizations, you should uninstall the Magento software and reinstall it with the released version. Uninstalling not only drops the database tables, it also clears the Magento `var` directory, enabling you to start over with no issues. + + For more information, see [Change versions by uninstalling the Magento software](#downgrade-uninstall) + +* If you have done customizations and do not want to lose them, back up the Magento system, switch to the released branch, and install in a new database instance. + + For more information, see [Change versions by installing the Magento software in a new database instance](#downgrade-db) + + You can migrate your customizations (both in the file system and in the database) from the backups you made or directly using database and file system tools. + +### Change versions by uninstalling the Magento software {#downgrade-uninstall} + +To change versions after cloning: + +1. Log in to your Magento server as, or switch to, [the file system owner]({{ page.baseurl }}/install-gde/prereq/file-sys-perms-over.html). +1. Use the following command to uninstall the Magento software: + + ```bash + php /bin/magento setup:uninstall + ``` + +1. Either remove your old Magento clone directory or [update the Magento software]({{ page.baseurl }}/install-gde/install/cli/dev_update-magento.html). +1. If you have not already done so, clone the Magento 2 GitHub repository as follows: + + ```bash + git clone git@github.com:magento/magento2.git + ``` + +1. Change to [release tag](https://github.com/magento/magento2/tags) as follows: + + ```bash + git checkout tags/ [-b ] + ``` + + For example, to check out the 2.2.0 release tag in a new branch named `2.2.0`, enter + + ```bash + git checkout tags/2.2.0 -b 2.2.0 + ``` + +1. Install the Magento software using the [command line]({{ page.baseurl }}/install-gde/install/cli/install-cli-install.html). + +### Change versions by installing the Magento software in a new database instance {#downgrade-db} + +To change versions after cloning: + +1. Log in to your Magento server as, or switch to, [the file system owner]({{ page.baseurl }}/install-gde/prereq/file-sys-perms-over.html). +1. Create a [new database instance]({{ page.baseurl }}/install-gde/prereq/mysql.html#instgde-prereq-mysql-config) for your installation. +1. [Back up]({{ page.baseurl }}/install-gde/install/cli/install-cli-backup.html#instgde-cli-uninst-back) the Magento file system, database, and media files: + + ```bash + php /bin/magento setup:backup --code --media --db + ``` + +1. Change to [release tag](https://github.com/magento/magento2/tags) as follows: + + ```bash + git checkout tags/ [-b ] + ``` + + For example, to check out the 2.2.0 release tag in a new branch named `2.2.0`, enter + + ```bash + git checkout tags/2.2.0 -b 2.2.0 + ``` + +1. Manually clear Magento `var` directories: + + ```bash + rm -rf /var/cache/* /var/page_cache/* /generated/code/* + ``` + +1. Install the Magento software in your new database instance. + + You must install using the [command line]({{ page.baseurl }}/install-gde/install/cli/install-cli-install.html). diff --git a/src/guides/v2.3/install-gde/install/cli/dev_reinstall.md b/src/guides/v2.3/install-gde/install/cli/dev_reinstall.md new file mode 100644 index 00000000000..f2289a6afb0 --- /dev/null +++ b/src/guides/v2.3/install-gde/install/cli/dev_reinstall.md @@ -0,0 +1,55 @@ +--- +subgroup: 99_contrib +title: Reinstall the Magento software +menu_title: Reinstall the Magento software +menu_order: 200 +menu_node: +functional_areas: + - Install + - System + - Setup +--- + +A contributing developer reinstalls Magento by updating `composer.json` to specify the Magento product version and component versions desired and runs `composer update`. + +To reinstall the Magento software as a contributing developer: + +1. Log in to your Magento server as a user with permissions to modify files in the Magento file system (for example, the [switch to the file system owner]({{ page.baseurl }}/install-gde/prereq/file-sys-perms-over.html). +1. Make a backup copy of `composer.json` in your Magento installation directory: + + ```bash + cd + ``` + + ```bash + cp composer.json composer.json.bak + ``` + +1. Open `composer.json` in a text editor. +1. Locate the following line: + + ```json + "require": { + "magento/product-community-edition": "" + }, + ``` + +1. Replace `` with the version to which you want to upgrade, where `` is the product version to use. + + (The product version is in the format `2.0.x`) + +. + +1. Save your changes to `composer.json` and exit the text editor. +1. Enter the following command: + + ```bash + composer update + ``` + + Wait for dependencies to update. + +1. [Install the Magento software]({{ page.baseurl }}/install-gde/install/cli/install-cli.html). + +*[contributing developer]: A developer who contributes code to the Magento 2 CE codebase +*[contributing developers]: Developers who contribute code to the Magento 2 CE codebase diff --git a/src/guides/v2.3/install-gde/install/cli/install-cli-adminurl.md b/src/guides/v2.3/install-gde/install/cli/install-cli-adminurl.md new file mode 100644 index 00000000000..9d4519650f6 --- /dev/null +++ b/src/guides/v2.3/install-gde/install/cli/install-cli-adminurl.md @@ -0,0 +1,43 @@ +--- +title: Display or change the Admin URI +functional_areas: + - Install + - System + - Setup +--- + +## First steps {#instgde-cli-before} +{% include install/first-steps-cli.md %} +In addition to the command arguments discussed here, see [Common arguments]({{ page.baseurl }}/install-gde/install/cli/install-cli-subcommands.html#instgde-cli-subcommands-common). + +## Prerequisites {#instgde-cli-subcommands-db-prereq} + +Before you run this command, you must [Create or update the deployment configuration]({{ page.baseurl }}/install-gde/install/cli/install-cli-subcommands-deployment.html). + +## Display the Admin URI {#instgde-cli-displayurl} +This section discusses how to use the command line to display the [Admin](https://glossary.magento.com/admin) Uniform Resource Identifier ([URI](https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.2)). + +Command options: + +```bash +bin/magento info:adminuri +``` + +A sample result follows: + +```terminal +Admin Panel URI: /admin_1wgrah +``` + +You can also view the Admin URI in `/app/etc/env.php`. A snippet follows: + +```php?start_inline=1 + 'backend' => + array ( + 'frontName' => 'admin_1wgrah', + ), +``` + +## Change the Admin URL {#instgde-cli-changeurl} + +To change the Admin URI, use the [magento setup:config:set]({{ page.baseurl }}/install-gde/install/cli/install-cli-subcommands-deployment.html) command. \ No newline at end of file diff --git a/src/guides/v2.3/install-gde/install/cli/install-cli-backup.md b/src/guides/v2.3/install-gde/install/cli/install-cli-backup.md new file mode 100644 index 00000000000..5f58a55121b --- /dev/null +++ b/src/guides/v2.3/install-gde/install/cli/install-cli-backup.md @@ -0,0 +1,115 @@ +--- +title: Back up and roll back the file system, media, and database +functional_areas: + - Install + - System + - Setup +--- + +## Overview of backup {#instgde-cli-uninst-back-over} + +This command enables you to back up: + +* The Magento file system (excluding `var` and `pub/static` directories) +* The `pub/media` directory +* The Magento 2 database + +Backups are stored in the `var/backups` directory and can be restored at any time using the [magento setup:rollback]({{ page.baseurl }}/install-gde/install/cli/install-cli-uninstall-mods.html#instgde-cli-uninst-mod-roll) command. + +After backing up, you can [roll back](#instgde-cli-uninst-roll) at a later time. + +{:.bs-callout-tip} +For {{site.data.var.ece}} projects, see [Snapshots and backup management]({{ site.baseurl }}/cloud/project/project-webint-snap.html) in the _Cloud guide_. + +## First steps {#instgde-cli-before} +{% include install/first-steps-cli.md %} +In addition to the command arguments discussed here, see [Common arguments]({{ page.baseurl }}/install-gde/install/cli/install-cli-subcommands.html#instgde-cli-subcommands-common). + +## Enable backups + +The Magento backup feature is disabled by default. To enable, enter the following CLI command: + +```bash +bin/magento config:set system/backup/functionality_enabled 1 +``` + +{:.bs-callout-warning} +**Deprecation Notice:** +Magento backup functionality is deprecated as of 2.1.16, 2.2.7, and 2.3.0. We recommend investigating additional backup technologies and binary backup tools (such as Percona XtraBackup). + +## Set the open files limit {#instgde-cli-ulimit} +{% include install/ulimit.md %} + +## Backing up {#instgde-cli-uninst-back} + +Command usage: + +```bash +bin/magento setup:backup [--code] [--media] [--db] +``` + +The command performs the following tasks: + +1. Puts the store in maintenance mode. +1. Executes one of the following command options. + + |Option|Meaning|Backup file name and location| + |--- |--- |--- | + |`--code`|Backs up the Magento file system (excluding var and pub/static directories).|var/backups/\_filesystem.tgz| + |`--media`|Back up the pub/media directory.|var/backups/\_filesystem_media.tgz| + |`--db`|Back up the Magento 2 database.|var/backups/\_db.sql| + +1. Takes the store out of maintenance mode. + +For example, to back up the file system and database, + +```bash +bin/magento setup:backup --code --db +``` + +Messages similar to the following display: + +```terminal +Enabling maintenance mode +Code backup is starting... +Code backup filename: 1434133011_filesystem.tgz (The archive can be uncompressed with 7-Zip on Windows systems) +Code backup path: /var/www/html/magento2/var/backups/1434133011_filesystem.tgz +[SUCCESS]: Code backup completed successfully. +DB backup is starting... +DB backup filename: 1434133011_db.sql +DB backup path: /var/www/html/magento2/var/backups/1434133011_db.sql +[SUCCESS]: DB backup completed successfully. +Disabling maintenance mode +``` + +## Roll back {#instgde-cli-uninst-roll} + +This section discusses how to roll back to a backup you made previously. You must know the file name of the backup file to restore. + +To find the name of your backups, enter: + +```bash +bin/magento info:backups:list +``` + +The first string in the backup file name is the timestamp. + +To roll back to a previous backup, enter: + +```bash +bin/magento setup:rollback [-c|--code-file=""] [-m|--media-file=""] [-d|--db-file=""] +``` + +For example, to restore a media backup named `1440611839_filesystem_media.tgz`, enter + +```bash +bin/magento setup:rollback -m 1440611839_filesystem_media.tgz +``` + +Messages similar to the following display: + +```terminal +[SUCCESS]: Media rollback completed successfully. +Please set file permission of bin/magento to executable +Disabling maintenance mode +``` diff --git a/src/guides/v2.3/install-gde/install/cli/install-cli-install.md b/src/guides/v2.3/install-gde/install/cli/install-cli-install.md new file mode 100644 index 00000000000..0d898730bcf --- /dev/null +++ b/src/guides/v2.3/install-gde/install/cli/install-cli-install.md @@ -0,0 +1,261 @@ +--- +title: Install the Magento software +functional_areas: + - Install + - System + - Setup +--- + +## Before you start your installation {#instgde-install-cli-prereq} + +Before you begin, complete the following steps: + +* Verify that your system meets the requirements discussed in [Magento system requirements]({{ page.baseurl }}/install-gde/system-requirements.html). + +* Complete all [prerequisite]({{ page.baseurl }}/install-gde/prereq/prereq-overview.html) tasks. + +* Complete the first installation steps. See [Your install or upgrade path]({{ page.baseurl }}/install-gde/bk-install-guide.html). + +* After you log in to the Magento server, [switch to the file system owner]({{ page.baseurl }}/install-gde/prereq/file-sys-perms-over.html). + +* Review the [Get started with the command-line installation]({{ page.baseurl }}/install-gde/install/cli/install-cli-subcommands.html) overview. + + {:.bs-callout-info} +You must install Magento from its `bin` subdirectory. + +You can run the installer multiple times with different options to complete installation tasks like the following: + +* Install in phases—For example, after you configure your web server for Secure Sockets Layer (SSL), you can run the installer again to set SSL options. + +* Correct mistakes in previous installations. + +* Install Magento in a different database instance. + + {:.bs-callout-info} +By default, the installer does not overwrite the Magento database if you install the Magento software in the same database instance. You can use the optional `cleanup-database` parameter to change this behavior. + +See also [Update, reinstall, uninstall]({{ page.baseurl }}/install-gde/install/cli/install-cli-uninstall.html). + +{% include install/fully-secure.md %} + +## Installer help commands {#instgde-cli-help-cmds} + +You can run the following commands to find values for some required arguments: + +| Installer argument | Command | +| ------------------ | ------------------------------- | +| Language | magento info:language:list | +| Currency | magento info:currency:list | +| Time zone | php magento info:timezone:list | + + {:.bs-callout-info} +If an error displays when you run these commands, verify that you updated installation dependencies as discussed in [Update installation dependencies]({{ page.baseurl }}/install-gde/install/prepare-install.html). + +## Install the Magento software from the command line {#instgde-install-cli-magento} + +The install command uses the following format: + +```bash +bin/magento setup:install --
    '; +``` + +## Additional naming convention standards + +### General naming conventions + +* Avoid underscores and numbers in names. +* Variables or methods should have names that accurately describe their purpose or behavior. +* Object methods or variables that are declared `private` or `protected` should start with an underscore(`_`). + +### Functions and methods + +* Class method names should start with an English verb in its infinitive form that describes the method. +* Names for accessors for instance or static variables should always have the `get` or `set` prefix. +* In [design pattern](https://glossary.magento.com/design-pattern) classes, implementation method names should contain the pattern name where practical to provide better behavior description. +* Methods that return status flags or Boolean values should have the `has` or `is` prefix. + +### Variables and properties + +* Do not use short variable names such as `i` or `n` except in small loop contexts +* If a loop contains more than 20 lines of code, the index variables should have more descriptive names. + +## Additional coding construct standards + +### Binary and ternary operators + +Always put the operator on the preceding line to avoid implicit semi-colon insertion issues. + +### Custom `toString()` method + +This method must always succeed without side effects. + +### Function declarations within blocks + +Use a variable initialized with a function expression to define a function within a block. + +```javascript +// Wrong +if (x) { + function foo() {} +} + +// Correct +if (x) { + var foo = function() {} +} +``` + +### Exceptions and custom exceptions + +You cannot avoid exceptions if you are doing something non-trivial (using an application development framework, and so on). + +Without custom exceptions, returning error information from a function that also returns a value can be tricky, not to mention inelegant. +Bad solutions include passing in a reference type to hold error information or always returning Objects with a potential error member. + +These basically amount to a primitive [exception](https://glossary.magento.com/exception) handling hack. +Feel free to use custom exceptions when appropriate. + +### Standard features {#standard-features} + +For maximum portability and compatibility, use standard features whenever possible. + +For example, `string.charAt(3)` instead of `string[3]`, and element access with DOM functions instead of using an application-specific shorthand. + +### Method definitions + +There are several ways to attach methods and properties to a constructor, but the preferred style is: + +```javascript +Foo.prototype.bar = function() { + // ... +}; +``` + +Do not use: + +```javascript +Foo.prototype = { + bar: function() { + // ... + }, + circle: function() { + // ... + } +}; + +``` + +Assignment operations to constructor prototypes creating temporal coupling and sometimes other unwanted side effects. + +### Closures + +A closure keeps a pointer to its enclosing scope, so attaching a closure to a DOM element can create a circular reference and thus, a memory leak. + +```javascript +// Wrong +function foo(element, a, b) { + element.onclick = function() { + // uses a and b + }; +} +``` + +The function closure keeps references to elements "a" and "b" even if it never uses them. + +Because elements also keep references to the closure, it is a cycle that will not be cleaned up by garbage collection. +In these situations, the code can be structured as follows: + +```javascript +// Correct +function foo(element, a, b) { + element.onclick = bar(a, b); +} + +function bar(a, b) { + return function() { + // uses a and b + } +} +``` + +## Additional general standards + +### Array and object initializers + +Single-line array and object initializers are allowed when they fit on a line as follows: + +```javascript + var arr = [1, 2, 3]; // No space after [ or before ]. + var obj = {a: 1, b: 2, c: 3}; // No space after { or before }. +``` + +Long identifiers or values present problems for aligned initialization lists, so always prefer non-aligned initialization. + +For example: + +```javascript +Object.prototype = { + a: 0, + b: 1, + lengthyName: 2 +}; +``` + +### Associative arrays + +Use `Object` instead of `Array` for associative arrays. + +### Deferred initialization + +Use deferred initialization when it is not possible to initialize variables at the point of declaration. + +### Explicit scope + +Use explicit scope to increase code portability and clarity. + +### Built-in objects + +Modifying built-in like `Object.prototype` and `Array.prototype` is strictly forbidden. + +Modifying other built-ins like `Function.prototype` is less dangerous but leads to debugging issue in production. + +### Variable declarations + +Declare a variable with `var` wherever possible to avoid overwriting existing global values. + +Using only one var per scope promotes readability. + +```javascript +var foo = 'bar', + num = 1, + arr = [1, 2, 3]; +``` + +## Custom rules + +There is a set of custom Eslint rules to ensure code compatiblity with the latest versions of 3rd party libraries. + +These custom rules are included using the `rulePaths` setting in the [Eslint Grunt configuration][grunt-eslint-configuration]. + +The source code of the rules can be found in the [Eslint custom rules folder][eslint-custom-rules-folder]. + +[jquery]: https://jquery.com/ +[jquery-widgets]: https://api.jqueryui.com/category/widgets +[jquery-widget-coding-standard]: {{ page.baseurl }}/coding-standards/code-standard-jquery-widgets.html +[eslint]: https://eslint.org/ +[eslint-rules]: https://github.com/magento/magento-coding-standard/blob/develop/eslint/.eslintrc-magento +[grunt-eslint-configuration]: {{ site.mage2bloburl }}/{{ page.guide_version }}/dev/tools/grunt/configs/eslint.json +[eslint-custom-rules-folder]: {{ site.mage2bloburl }}/{{ page.guide_version }}/dev/tests/static/testsuite/Magento/Test/Js/_files/eslint/rules/ diff --git a/src/guides/v2.4/coding-standards/code-standard-jquery-widgets.md b/src/guides/v2.4/coding-standards/code-standard-jquery-widgets.md new file mode 100644 index 00000000000..6251a40ecdb --- /dev/null +++ b/src/guides/v2.4/coding-standards/code-standard-jquery-widgets.md @@ -0,0 +1,357 @@ +--- +group: coding-standards +subgroup: 01_Coding standards +title: jQuery widget coding standard +landing-page: Coding standards +menu_title: jQuery widget coding standard +menu_order: 7 +functional_areas: + - Standards +redirect_to: https://developer.adobe.com/commerce/php/coding-standards/jquery-widgets/ +status: migrated +--- + +In the Magento system, all jQuery UI widgets and interactions are built on a simple, reusable base---the [jQuery UI Widget Factory][jquery-ui-widget-factory]. + +The factory provides a flexible base for building complex, stateful plug-ins with a consistent [API](https://glossary.magento.com/api). +It is designed not only for plug-ins that are part of [jQuery](https://glossary.magento.com/jquery) UI, but for general usage by developers who want to create object-oriented components without reinventing common infrastructure. + +For more information, see the [jQuery Widget API documentation][jquery-ui-api-doc]. + +This standard is mandatory for Magento core developers and recommended for third-party [extension](https://glossary.magento.com/extension) developers. +Some parts of Magento code might not comply with the standard, but we are working to gradually improve this. + +Use [RFC 2119][rfc2119] to interpret the "must," "must not," "required," "shall," "shall not," "should," "should not," "recommended," "may," and "optional" keywords. + +## Naming conventions + +* [Widget](https://glossary.magento.com/widget) names must consist of one or more non-abbreviated English word and in camelcase format. + + ```javascript + (function($) { + $.widget('mage.accordion', $.ui.accordion, { + // ... My custom code ... + }); + ``` + +* Widget names should be verbose enough to fully describe their purpose and behavior. + + ```javascript + // Declaration of the frontend.advancedEventTrigger widget + (function($) { + "use strict"; + + $.widget('mage.advancedEventTrigger', $.ui.button, { + // ... My custom code ... + }); + }) (jQuery); + ``` + +## Instantiation and resources + +* Additional [JavaScript](https://glossary.magento.com/javascript) files used as a resources must be dynamically loaded using the `$.mage.components()` method and must not be included in the `` block. +* Use the `$.mage.components()` method to load additional JavaScript resource files not included in the `` block. +* You must use `$.mage.extend()` to extend an existing set of widget resources. +* You must instantiate widgets using the `data-mage-init` attribute. + You can use the `.mage()` [plug-in](https://glossary.magento.com/plug-in) to instantiate widgets that use callback methods. + + Benefits: + + * You leverage the benefits of `$.mage.extend()` and `$.mage.components()`. + * Using `data-mage-init` minimizes the inline JavaScript code footprint. + * You can modify widget initialization parameters. + + ```javascript + // Widget initialization using the data-mage-init attribute +
    + + // Widget initialization using the mage plug-in + (function($) { + $('selector').mage('dialog', { + close: function(e) { + $(this).dialog('destroy'); + } + }); + })(jQuery); + ``` + +* You can declare callback methods inline JavaScript but not methods and widgets. + + ```javascript + // Widget initialization and configuration + $('selector').mage('dialog', { + close: function(e) { + $(this).dialog('destroy'); + } + }); + + // Widget initialization and binding event handlers + $('selector').mage('dialog').on('dialogclose', { + $(this).dialog('destroy'); + }); + + // Extension for widget in a JavaScript file + $.widget('mage.dialog', $.ui.dialog, { + close: function() { + this.destroy(); + } + }); + + // Extension of widget resources + (function($) { + $.mage + .extend('dialog', 'dialog', + 'getViewFileUrl('Enterprise_\*Module\*::page/js/dialog.js') ?>') + })(jQuery); + ``` + +### Initializing a component on a selector + +There are two ways to initialize a component on a selector: + +* Initialize the component in the `data-mage-init` attribute: + + ```html +
    + ``` + +* Use a script type `text/x-magento-init` attribute: + + ```html + + ``` + +In these cases the path to the file is: + + `Vendor/Module/view/frontend/web/js/jsfilename.js` + + which contains your code: + + ```javascript + define(['uiComponent'], + function (Component) { + 'use strict'; + return Component.extend({ + initialize: function (config, node) { + // some code + } + }); + }); + ``` + +### Initializing a component on a selector with parameters + +When a component is initialized, it is also important to send parameters to it, which are normally determined dynamically in PHP. + +* `data-mage-init` + + ```html +
    + ``` + +* Using a script type `text/x-magento-init` attribute. For example: + + ```html + + ``` + +## Development standards + +* Widgets should comply with the [single responsibility principle][single-responsibility-principle]. + + Widgets should not have responsibilities not related to the [entity](https://glossary.magento.com/entity) described by the widget. + + ```javascript + // Widget "dialog" that is responsible + // only for opening content in an interactive overlay. + $.widget('mage.dialog', { + // Code logic + }); + + // Widget "validation" that is responsible + // only for validating the form fields. + $.widget('mage.validation', $.ui.sortable, { + // Code logic + }); + + $('selector') + .mage('dialog') + .find('form') + .mage('validation'); + ``` + +* Widget properties that modify the widget's behavior must be located in the widget's options to make them configurable and reusable. + + ```javascript + //Declaration of the backend.dialog widget + $.widget('mage.dialog', { + options: { + modal: false, + autoOpen: true, + // Additional widget options + }, + // Additional widget properties + }); + + // Initializing + $('selector').mage('dialog', { + modal: true, + autoOpen: false + }); + ``` + +* Widget communications must be handled by jQuery events + + ```html + + ... +