diff --git a/.changeset/bright-horses-peel.md b/.changeset/bright-horses-peel.md deleted file mode 100644 index ac95e6b3a3c..00000000000 --- a/.changeset/bright-horses-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": minor ---- - -fixes failing test: product visible in shop SALEOR_2506 diff --git a/.changeset/brown-readers-hang.md b/.changeset/brown-readers-hang.md deleted file mode 100644 index 5c652277efd..00000000000 --- a/.changeset/brown-readers-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix sending attibutes on variant create/update in datagrid on product details page diff --git a/.changeset/brown-years-wash.md b/.changeset/brown-years-wash.md deleted file mode 100644 index cf586db92ce..00000000000 --- a/.changeset/brown-years-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix sending too many request on line item update diff --git a/.changeset/curly-rules-sin.md b/.changeset/curly-rules-sin.md deleted file mode 100644 index e12d7f14558..00000000000 --- a/.changeset/curly-rules-sin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Updates data-test-id for variant name input on variant page diff --git a/.changeset/curly-zoos-do.md b/.changeset/curly-zoos-do.md deleted file mode 100644 index c428b5117d4..00000000000 --- a/.changeset/curly-zoos-do.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Add 'feature' as extempt label for stalebot diff --git a/.changeset/curvy-bulldogs-drum.md b/.changeset/curvy-bulldogs-drum.md deleted file mode 100644 index 1312450159b..00000000000 --- a/.changeset/curvy-bulldogs-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Use changes files to detect if changeset file is present diff --git a/.changeset/dirty-dragons-film.md b/.changeset/dirty-dragons-film.md deleted file mode 100644 index f234946f4af..00000000000 --- a/.changeset/dirty-dragons-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix pasting data into page create type picker diff --git a/.changeset/empty-falcons-boil.md b/.changeset/empty-falcons-boil.md new file mode 100644 index 00000000000..726fea08faa --- /dev/null +++ b/.changeset/empty-falcons-boil.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Introduce intial component for catalog discounts diff --git a/.changeset/fast-masks-deny.md b/.changeset/fast-masks-deny.md deleted file mode 100644 index b7285eeec74..00000000000 --- a/.changeset/fast-masks-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix input placeholder text in the tax classes section diff --git a/.changeset/fast-tips-sparkle.md b/.changeset/fast-tips-sparkle.md new file mode 100644 index 00000000000..b41afe28290 --- /dev/null +++ b/.changeset/fast-tips-sparkle.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Add exemption labels to ignore stale issues diff --git a/.changeset/few-drinks-compete.md b/.changeset/few-drinks-compete.md deleted file mode 100644 index e67def19a06..00000000000 --- a/.changeset/few-drinks-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Remove flaky product update page test diff --git a/.changeset/fifty-peas-cheer.md b/.changeset/fifty-peas-cheer.md deleted file mode 100644 index a1376cb6304..00000000000 --- a/.changeset/fifty-peas-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix form blink in customer edition for orders diff --git a/.changeset/fifty-weeks-teach.md b/.changeset/fifty-weeks-teach.md new file mode 100644 index 00000000000..bfdea5dd627 --- /dev/null +++ b/.changeset/fifty-weeks-teach.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Fix duplicates when assigning reference attributes & eligible entities for vouchers and products diff --git a/.changeset/five-eggs-brake.md b/.changeset/five-eggs-brake.md deleted file mode 100644 index a845151cc84..00000000000 --- a/.changeset/five-eggs-brake.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/.changeset/fluffy-buckets-tell.md b/.changeset/fluffy-buckets-tell.md deleted file mode 100644 index e9806120dab..00000000000 --- a/.changeset/fluffy-buckets-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix GHA worflow that runs chromatic on main branch diff --git a/.changeset/proud-shirts-hammer.md b/.changeset/four-news-glow.md similarity index 52% rename from .changeset/proud-shirts-hammer.md rename to .changeset/four-news-glow.md index 8db023518e0..215242ecc63 100644 --- a/.changeset/proud-shirts-hammer.md +++ b/.changeset/four-news-glow.md @@ -2,4 +2,4 @@ "saleor-dashboard": minor --- -Removed unused get info request +add env var LOCALE_CODE diff --git a/.changeset/fresh-forks-battle.md b/.changeset/fresh-forks-battle.md deleted file mode 100644 index fe688d303e1..00000000000 --- a/.changeset/fresh-forks-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix saving previously removed country exceptions diff --git a/.changeset/honest-otters-bow.md b/.changeset/honest-otters-bow.md deleted file mode 100644 index cc923742199..00000000000 --- a/.changeset/honest-otters-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": minor ---- - -Use macaw-ui alias to 1.0.0 versions diff --git a/.changeset/itchy-pumas-sip.md b/.changeset/itchy-pumas-sip.md new file mode 100644 index 00000000000..0e7bd76fe75 --- /dev/null +++ b/.changeset/itchy-pumas-sip.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Use stale action instead of stalebot diff --git a/.changeset/lovely-walls-shake.md b/.changeset/lovely-walls-shake.md deleted file mode 100644 index 337a36f01b3..00000000000 --- a/.changeset/lovely-walls-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": minor ---- - -Introduce voucher codes datagrid diff --git a/.changeset/metal-cows-yawn.md b/.changeset/metal-cows-yawn.md deleted file mode 100644 index 34595fcd04d..00000000000 --- a/.changeset/metal-cows-yawn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": minor ---- - -Migrated "TC: SALEOR_26 Create basic info variant - via edit variant page" to playwright diff --git a/.changeset/moody-countries-dream.md b/.changeset/moody-countries-dream.md new file mode 100644 index 00000000000..2489d114302 --- /dev/null +++ b/.changeset/moody-countries-dream.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Fix drag and drop in navigation configuration diff --git a/.changeset/ninety-pillows-sing.md b/.changeset/ninety-pillows-sing.md deleted file mode 100644 index 4854b907514..00000000000 --- a/.changeset/ninety-pillows-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix error related to can not read property of undefined in metadata and reading search page info diff --git a/.changeset/olive-walls-joke.md b/.changeset/olive-walls-joke.md deleted file mode 100644 index 24ad1bbeee3..00000000000 --- a/.changeset/olive-walls-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": minor ---- - -Home page critical test migration to playwright diff --git a/.changeset/perfect-ligers-hope.md b/.changeset/perfect-ligers-hope.md deleted file mode 100644 index cfc7b1deeda..00000000000 --- a/.changeset/perfect-ligers-hope.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": minor ---- - -migrated navigation tests to playwright diff --git a/.changeset/tricky-needles-brush.md b/.changeset/rude-falcons-peel.md similarity index 53% rename from .changeset/tricky-needles-brush.md rename to .changeset/rude-falcons-peel.md index 7bdb5ada186..bea7b15d904 100644 --- a/.changeset/tricky-needles-brush.md +++ b/.changeset/rude-falcons-peel.md @@ -2,4 +2,4 @@ "saleor-dashboard": patch --- -Fix language switcher +Improve channel delete dialogs diff --git a/.changeset/short-deers-learn.md b/.changeset/short-deers-learn.md deleted file mode 100644 index 9aee0d9e113..00000000000 --- a/.changeset/short-deers-learn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": minor ---- - -Migrated warehouse creat test to playwright diff --git a/.changeset/six-deers-exist.md b/.changeset/six-deers-exist.md new file mode 100644 index 00000000000..079707b1bd1 --- /dev/null +++ b/.changeset/six-deers-exist.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Add missing units for attributes diff --git a/.changeset/soft-hornets-relax.md b/.changeset/soft-hornets-relax.md deleted file mode 100644 index f9d0b5a439a..00000000000 --- a/.changeset/soft-hornets-relax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Change e2e default browser to electron diff --git a/.changeset/spotty-files-dance.md b/.changeset/spotty-files-dance.md deleted file mode 100644 index ab0aaf670c7..00000000000 --- a/.changeset/spotty-files-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix assigning products to collection diff --git a/.changeset/stale-shirts-lie.md b/.changeset/stale-shirts-lie.md deleted file mode 100644 index 52811fa3dce..00000000000 --- a/.changeset/stale-shirts-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix pasting float number into datagrid diff --git a/.changeset/ten-wasps-raise.md b/.changeset/ten-wasps-raise.md deleted file mode 100644 index bd631665eee..00000000000 --- a/.changeset/ten-wasps-raise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": minor ---- - -Migrated Create basic order test to playwright diff --git a/.changeset/tidy-planes-wash.md b/.changeset/tidy-planes-wash.md deleted file mode 100644 index d48b9b48d31..00000000000 --- a/.changeset/tidy-planes-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Update selector for orders tests in cypress diff --git a/.changeset/tiny-pumas-leave.md b/.changeset/tiny-pumas-leave.md new file mode 100644 index 00000000000..c14d9bd29ec --- /dev/null +++ b/.changeset/tiny-pumas-leave.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Create instance when it has been not created yet diff --git a/.changeset/tricky-feet-type.md b/.changeset/tricky-feet-type.md deleted file mode 100644 index 311cbc371b7..00000000000 --- a/.changeset/tricky-feet-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix clear datagrid added variants after submit diff --git a/.changeset/twenty-shrimps-breathe.md b/.changeset/twenty-shrimps-breathe.md deleted file mode 100644 index b4b062c0911..00000000000 --- a/.changeset/twenty-shrimps-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": minor ---- - -Migrated shipping methods tests to playwright diff --git a/.changeset/twenty-snails-retire.md b/.changeset/twenty-snails-retire.md deleted file mode 100644 index cd3f2947066..00000000000 --- a/.changeset/twenty-snails-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix copy on back button when failure during apo installation diff --git a/.changeset/two-bears-happen.md b/.changeset/two-bears-happen.md new file mode 100644 index 00000000000..26badfcd947 --- /dev/null +++ b/.changeset/two-bears-happen.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Display warning for long branch names diff --git a/.changeset/wicked-planes-lick.md b/.changeset/wicked-planes-lick.md new file mode 100644 index 00000000000..7a35b76449b --- /dev/null +++ b/.changeset/wicked-planes-lick.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": patch +--- + +Prevent empty subscription query for new webhooks diff --git a/.changeset/young-spoons-count.md b/.changeset/young-spoons-count.md deleted file mode 100644 index d5ffc35a392..00000000000 --- a/.changeset/young-spoons-count.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"saleor-dashboard": patch ---- - -Fix disappearing labels of reference attributes diff --git a/.env.template b/.env.template index e9ef865c22d..184b914d84c 100644 --- a/.env.template +++ b/.env.template @@ -1,3 +1,4 @@ API_URI=https://demo.saleor.io/graphql/ APP_MOUNT_URI=/ -APPS_MARKETPLACE_API_URI=https://apps.saleor.io/api/v2/saleor-apps \ No newline at end of file +APPS_MARKETPLACE_API_URI=https://apps.saleor.io/api/v2/saleor-apps +LOCALE_CODE="EN" \ No newline at end of file diff --git a/.featureFlags/generated.tsx b/.featureFlags/generated.tsx index 119f2612b77..41125a51af5 100644 --- a/.featureFlags/generated.tsx +++ b/.featureFlags/generated.tsx @@ -1,13 +1,11 @@ // @ts-nocheck -import O81816 from "./images/filters.png" +import D31948 from "./images/filters.png" -const product_filters = () => (<>

new filters

+const product_filters = () => (<>

new filters

Experience the new look and enhanced abilities of new fitering mechanism. Easily combine any criteria you want, and quickly browse their values.

) -const voucher_codes = () => (<>

Allow to generat multple codes per single voucher

-) export const AVAILABLE_FLAGS = [{ name: "product_filters", @@ -18,13 +16,4 @@ export const AVAILABLE_FLAGS = [{ enabled: true, payload: "default", } -},{ - name: "voucher_codes", - displayName: "Voucher codes", - component: voucher_codes, - visible: false, - content: { - enabled: false, - payload: "default", - } }] as const; diff --git a/.featureFlags/voucher-codes.md b/.featureFlags/voucher-codes.md deleted file mode 100644 index 69938b52504..00000000000 --- a/.featureFlags/voucher-codes.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: voucher_codes -displayName: Voucher codes -enabled: false -payload: "default" -visible: false ---- - -Allow to generat multple codes per single voucher diff --git a/.github/ISSUE_TEMPLATE/enhancement_request.yml b/.github/ISSUE_TEMPLATE/enhancement_request.yml new file mode 100644 index 00000000000..395a2f58749 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement_request.yml @@ -0,0 +1,25 @@ +name: Enhancement request +description: "Small scope enhancements that do not alter the behavior of the product: Spelling, text adjustments, translations, documentation, small visual design corrections, icon adjustments, etc." +title: "[Enhancement]: " +labels: ["enhancement", "backlog"] + +body: + - type: textarea + id: description + attributes: + label: Description of the enhancement + description: Describe what you want to improve, attach the reasoning. + placeholder: | + Example: I want to adjust the border colors as well as inner padding to be consistent with the other views + validations: + required: true + - type: textarea + id: additional-info + attributes: + label: Additional information + description: Attach some additional references, links, some context + placeholder: | + Example: Currently we use old color values, we need to update it in the future + validations: + required: false + diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 2ef3ea1f5f5..ab12beff379 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,5 +1,5 @@ name: Feature request -description: Publish a new feature request +description: "Broader scope requests, a new implementation that is not yet present in the project." title: "[Feature]: " labels: ["feature", "backlog"] diff --git a/.github/actions/cli-login/action.yml b/.github/actions/cli-login/action.yml new file mode 100644 index 00000000000..95d8243ad50 --- /dev/null +++ b/.github/actions/cli-login/action.yml @@ -0,0 +1,15 @@ +name: Saleor CLI login +description: Saleor CLI login +inputs: + token: + description: "Cloud accces token" + required: true +runs: + using: "composite" + steps: + - name: Write config file + shell: bash + id: write-config-file + env: + ACCESS_TOKEN: ${{ inputs.token }} + run: jq --null-input --arg token "Token $ACCESS_TOKEN" '{"token":$token,"telemetry":"false","organization_slug":"saleor","organization_name":"Saleor"}' > ~/.config/saleor.json diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 0927b9e0099..00000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,63 +0,0 @@ -# Configuration for probot-stale - https://github.com/probot/stale - -# Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 60 - -# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. -# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. -daysUntilClose: 7 - -# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) -onlyLabels: [] - -# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable -exemptLabels: - - epic - - triage - - bug - - blocker - - backlog - - feature - -# Set to true to ignore issues in a project (defaults to false) -exemptProjects: false - -# Set to true to ignore issues in a milestone (defaults to false) -exemptMilestones: false - -# Set to true to ignore issues with an assignee (defaults to false) -exemptAssignees: false - -# Label to use when marking as stale -staleLabel: stale - -# Comment to post when marking as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. - -# Comment to post when removing the stale label. -# unmarkComment: > -# Your comment here. - -# Comment to post when closing a stale Issue or Pull Request. -# closeComment: > -# Your comment here. - -# Limit the number of actions per hour, from 1-30. Default is 30 -limitPerRun: 30 -# Limit to only `issues` or `pulls` -# only: issues - -# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': -# pulls: -# daysUntilStale: 30 -# markComment: > -# This pull request has been automatically marked as stale because it has not had -# recent activity. It will be closed if no further activity occurs. Thank you -# for your contributions. - -# issues: -# exemptLabels: -# - confirmed diff --git a/.github/workflows/changesets-status.yml b/.github/workflows/changesets-status.yml index 2a45a5b870d..1687ab3c9e9 100644 --- a/.github/workflows/changesets-status.yml +++ b/.github/workflows/changesets-status.yml @@ -6,16 +6,16 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Repo - uses: actions/checkout@v3 - - name: Extract branch name - id: extract_branch - run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT + uses: actions/checkout@v4 + with: + sparse-checkout: ./.changeset + - name: Changeset file lookup env: GH_TOKEN: ${{ github.token }} - PR_BRANCH: ${{ steps.extract_branch.outputs.branch }} + PR_ID: ${{ github.event.number }} run: | - files=$(gh pr diff "$PR_BRANCH" --name-only) + files=$(gh pr diff "$PR_ID" --name-only) if [[ $files =~ \.changeset\/.*.md ]]; then echo "Changesets found!" else diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml new file mode 100644 index 00000000000..6571a43f54d --- /dev/null +++ b/.github/workflows/pr-automation.yml @@ -0,0 +1,341 @@ +name: PR automation + +on: [pull_request] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + prepare_variables: + runs-on: ubuntu-22.04 + if: github.event.pull_request.head.repo.full_name == 'saleor/saleor-dashboard' + outputs: + POOL_NAME: ${{ steps.generate.outputs.POOL_NAME }} + POOL_INSTANCE: ${{ steps.generate.outputs.POOL_INSTANCE }} + BASE_URL: ${{ steps.generate.outputs.BASE_URL }} + API_URI: ${{ steps.generate.outputs.API_URI }} + BACKUP_ID: ${{ steps.backup.outputs.BACKUP_ID }} + BACKUP_VER: ${{ steps.backup.outputs.BACKUP_VER }} + BACKUP_NAME: ${{ steps.backup.outputs.BACKUP_NAME }} + + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: ./.github/actions + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@102b1a064a9b145e56556e22b18b19c624538d94 + + - name: Generate + id: generate + env: + PREFIX: pr- + run: | + echo "BASE_URL=${PREFIX}${GITHUB_HEAD_REF_SLUG_URL}.dashboard.saleor.rocks" >> $GITHUB_OUTPUT + echo "API_URI=https://${PREFIX}${GITHUB_HEAD_REF_SLUG_URL}.staging.saleor.cloud/graphql/" >> $GITHUB_OUTPUT + echo "POOL_NAME=${PREFIX}${GITHUB_HEAD_REF_SLUG_URL}" >> $GITHUB_OUTPUT + echo "POOL_INSTANCE=https://${PREFIX}${GITHUB_HEAD_REF_SLUG_URL}.staging.saleor.cloud" >> $GITHUB_OUTPUT + + - name: Saleor login + uses: ./.github/actions/cli-login + with: + token: ${{ secrets.STAGING_TOKEN }} + + - name: Obtain backup id + id: backup + env: + SALEOR_CLI_ENV: staging + BACKUP_NAME: snapshot-automation-tests + run: | + BACKUPS=$(npx saleor backup list --name=snapshot-automation-tests --latest --json) + BACKUP_ID=$(echo "$BACKUPS" | jq -r '.[0].key') + BACKUP_VER=$(echo "$BACKUPS" | jq -r '.[0].saleor_version') + BACKUP_NAME=$(echo "$BACKUPS" | jq -r '.[0].name') + + echo "BACKUP_ID=$BACKUP_ID" >> $GITHUB_OUTPUT + echo "BACKUP_VER=$BACKUP_VER" >> $GITHUB_OUTPUT + echo "BACKUP_NAME=$BACKUP_NAME" >> $GITHUB_OUTPUT + + - name: Print annotations + env: + BASE_URL: ${{ steps.generate.outputs.BASE_URL }} + API_URI: ${{ steps.generate.outputs.API_URI }} + POOL_NAME: ${{ steps.generate.outputs.POOL_NAME }} + POOL_INSTANCE: ${{ steps.generate.outputs.POOL_INSTANCE }} + BACKUP_ID: ${{ steps.backup.outputs.BACKUP_ID }} + BACKUP_VER: ${{ steps.backup.outputs.BACKUP_VER }} + BACKUP_NAME: ${{ steps.backup.outputs.BACKUP_NAME }} + run: | + echo "::notice title=BASE_URL::${BASE_URL}" + echo "::notice title=API_URI::${API_URI}" + echo "::notice title=POOL_NAME::${POOL_NAME}" + echo "::notice title=POOL_INSTANCE::${POOL_INSTANCE}" + echo "::notice title=SNAPSHOT::backup_id=${BACKUP_ID}, version=${BACKUP_VER}, name=${BACKUP_NAME}" + + prepare_instance: + runs-on: ubuntu-22.04 + needs: prepare_variables + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: ./.github/actions + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@102b1a064a9b145e56556e22b18b19c624538d94 + + - name: Saleor login + uses: ./.github/actions/cli-login + with: + token: ${{ secrets.STAGING_TOKEN }} + + - name: Instance check + id: instance_check + env: + SALEOR_CLI_ENV: staging + INSTANCE_NAME: ${{ needs.prepare_variables.outputs.POOL_NAME }} + run: | + INSTANCE_KEY=$(npx saleor env show "$INSTANCE_NAME" --json | jq .key) + echo "INSTANCE_KEY=$INSTANCE_KEY" >> $GITHUB_OUTPUT + + - name: Reload snapshot + if: ${{ steps.instance_check.outputs.INSTANCE_KEY }} + env: + SALEOR_CLI_ENV: staging + BACKUP_ID: ${{ needs.prepare_variables.outputs.BACKUP_ID }} + INSTANCE_NAME: ${{ needs.prepare_variables.outputs.POOL_NAME }} + run: | + npx saleor backup restore "$BACKUP_ID" \ + --environment="$INSTANCE_NAME" \ + --skip-webhooks-update + + - name: Create new instance + if: ${{ !steps.instance_check.outputs.INSTANCE_KEY }} + env: + SALEOR_CLI_ENV: staging + BACKUP_ID: ${{ needs.prepare_variables.outputs.BACKUP_ID }} + INSTANCE_NAME: ${{ needs.prepare_variables.outputs.POOL_NAME }} + run: | + npx saleor env create "$INSTANCE_NAME" \ + --project=project-for-pr-testing \ + --database=snapshot \ + --restore-from="$BACKUP_ID" \ + --saleor=saleor-master-staging \ + --domain="$INSTANCE_NAME" \ + --skip-restrict \ + --skip-webhooks-update + + deploy_dashboard: + if: github.event.pull_request.head.repo.full_name == 'saleor/saleor-dashboard' + runs-on: ubuntu-22.04 + needs: prepare_variables + permissions: + deployments: write + steps: + - uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + + - name: Start deployment + uses: bobheadxi/deployments@88ce5600046c82542f8246ac287d0a53c461bca3 + id: deployment + with: + step: start + token: ${{ secrets.GITHUB_TOKEN }} + env: ${{ needs.prepare_variables.outputs.POOL_NAME }} + ref: ${{ github.head_ref }} + + - name: Cache node modules + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-qa-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-qa-${{ env.cache-name }}- + ${{ runner.os }}-qa- + ${{ runner.os }}- + + - name: Install deps + run: npm ci + + - name: Build dashboard + env: + API_URI: ${{ needs.prepare_variables.outputs.API_URI }} + APPS_MARKETPLACE_API_URI: "https://apps.staging.saleor.io/api/v2/saleor-apps" + APP_MOUNT_URI: / + STATIC_URL: / + IS_CLOUD_INSTANCE: true + run: npm run build + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_DEFAULT_REGION }} + + - name: Deploy to S3 + env: + AWS_TEST_DEPLOYMENT_BUCKET: ${{ secrets.AWS_TEST_DEPLOYMENT_BUCKET }} + BASE_URL: ${{ needs.prepare_variables.outputs.BASE_URL }} + run: aws s3 sync ./build/dashboard "s3://${AWS_TEST_DEPLOYMENT_BUCKET}/${BASE_URL}" + + - name: Invalidate cache + env: + AWS_TEST_CF_DIST_ID: ${{ secrets.AWS_TEST_CF_DIST_ID }} + BASE_URL: ${{ needs.prepare_variables.outputs.BASE_URL }} + run: aws cloudfront create-invalidation --distribution-id "$AWS_TEST_CF_DIST_ID" --paths "/${BASE_URL}/*" + + - name: Update deployment status + uses: bobheadxi/deployments@88ce5600046c82542f8246ac287d0a53c461bca3 + if: always() + with: + step: finish + token: ${{ secrets.GITHUB_TOKEN }} + status: ${{ job.status }} + env_url: https://${{ needs.prepare_variables.outputs.BASE_URL }}/ + deployment_id: ${{ steps.deployment.outputs.deployment_id }} + env: ${{ needs.prepare_variables.outputs.POOL_NAME }} + + + deploy_storybook: + if: github.event.pull_request.head.repo.full_name == 'saleor/saleor-dashboard' + runs-on: ubuntu-22.04 + needs: prepare_variables + permissions: + deployments: write + steps: + - uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + + - name: Start storybook deployment + uses: bobheadxi/deployments@88ce5600046c82542f8246ac287d0a53c461bca3 + id: storybook-deployment + with: + step: start + token: ${{ secrets.GITHUB_TOKEN }} + env: storybook ${{ needs.prepare_variables.outputs.POOL_NAME }} + ref: ${{ github.head_ref }} + + - name: Cache node modules + uses: actions/cache@v3 + env: + cache-name: cache-node-modules + with: + path: ~/.npm + key: ${{ runner.os }}-qa-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-qa-${{ env.cache-name }}- + ${{ runner.os }}-qa- + ${{ runner.os }}- + + - name: Install deps + run: npm ci + + - name: Build storybook + run: npm run build-storybook + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v3 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_DEFAULT_REGION }} + + - name: Deploy to S3 + env: + AWS_TEST_DEPLOYMENT_BUCKET: ${{ secrets.AWS_TEST_DEPLOYMENT_BUCKET }} + BASE_URL: ${{ needs.prepare_variables.outputs.BASE_URL }} + run: aws s3 sync ./build/storybook "s3://${AWS_TEST_DEPLOYMENT_BUCKET}/${BASE_URL}/storybook" + + - name: Invalidate cache + env: + AWS_TEST_CF_DIST_ID: ${{ secrets.AWS_TEST_CF_DIST_ID }} + BASE_URL: ${{ needs.prepare_variables.outputs.BASE_URL }} + run: aws cloudfront create-invalidation --distribution-id "$AWS_TEST_CF_DIST_ID" --paths "/${BASE_URL}/*" + + - name: Update storybook deployment status + uses: bobheadxi/deployments@88ce5600046c82542f8246ac287d0a53c461bca3 + if: always() + with: + step: finish + token: ${{ secrets.GITHUB_TOKEN }} + status: ${{ job.status }} + env_url: https://${{ needs.prepare_variables.outputs.BASE_URL }}/storybook/index.html + deployment_id: ${{ steps.storybook-deployment.outputs.deployment_id }} + env: storybook ${{ needs.prepare_variables.outputs.POOL_NAME }} + + run-tests: + runs-on: ubuntu-latest + needs: [prepare_variables, deploy_dashboard, prepare_instance] + strategy: + fail-fast: false + matrix: + shard: [1/2, 2/2] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run tests + env: + API_URI: ${{ needs.prepare_variables.outputs.API_URI }} + BASE_URL: https://${{ needs.prepare_variables.outputs.BASE_URL }}/ + E2E_USER_NAME: ${{ secrets.CYPRESS_USER_NAME }} + E2E_USER_PASSWORD: ${{ secrets.CYPRESS_USER_PASSWORD }} + E2E_PERMISSIONS_USERS_PASSWORD: ${{ secrets.CYPRESS_PERMISSIONS_USERS_PASSWORD }} + run: npx playwright test --shard ${{ matrix.shard }} + + - name: Upload blob report to GitHub Actions Artifacts + uses: actions/upload-artifact@v3 + if: always() + with: + name: all-blob-reports + path: blob-report + retention-days: 1 + + merge-reports: + if: '!cancelled()' + needs: [run-tests] + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v3 + with: + name: all-blob-reports + path: all-blob-reports + + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter html ./all-blob-reports + + - name: Upload HTML report + uses: actions/upload-artifact@v3 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 14 diff --git a/.github/workflows/pr-cleanup.yml b/.github/workflows/pr-cleanup.yml new file mode 100644 index 00000000000..03ae4273ef3 --- /dev/null +++ b/.github/workflows/pr-cleanup.yml @@ -0,0 +1,31 @@ +name: PR cleanup + +on: + pull_request: + types: [closed] + +jobs: + remove_instance: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: ./.github/actions + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + + - name: Inject slug/short variables + uses: rlespinasse/github-slug-action@102b1a064a9b145e56556e22b18b19c624538d94 + + - name: Saleor login + uses: ./.github/actions/cli-login + with: + token: ${{ secrets.STAGING_TOKEN }} + + - name: Remove instance + env: + SALEOR_CLI_ENV: staging + run: npx saleor env remove "pr-${GITHUB_HEAD_REF_SLUG_URL}" --force \ No newline at end of file diff --git a/.github/workflows/stale-bot.yaml b/.github/workflows/stale-bot.yaml new file mode 100644 index 00000000000..4245518eb95 --- /dev/null +++ b/.github/workflows/stale-bot.yaml @@ -0,0 +1,22 @@ +name: Close stale issues and PRs +on: + schedule: + - cron: '30 1 * * *' # every day at 1:30am UTC + +jobs: + stale: + runs-on: ubuntu-22.04 + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v8 + with: + stale-issue-message: This issue is stale because it has been open 14 days with no activity. + stale-pr-message: This pull request is stale because it has been open 14 days with no activity. + close-issue-message: This issue was closed because it has been stalled for 2 days with no activity. You are still welcome to reopen it and continue from where you finished. Best regards Saleor team + close-pr-message: This PR request has been closed because it has been stalled for 2 days with no activity. You are still welcome to reopen it and continue from where you finished. Best regards Saleor team + days-before-stale: 14 + days-before-close: 2 + exempt-pr-labels: epic,triage,bug,blocker,backlog,feature + exempt-issue-labels: epic,triage,bug,blocker,backlog,feature diff --git a/.github/workflows/test-env-cleanup.yml b/.github/workflows/test-env-cleanup.yml index 1ea8811e268..bf695308077 100644 --- a/.github/workflows/test-env-cleanup.yml +++ b/.github/workflows/test-env-cleanup.yml @@ -1,4 +1,4 @@ -name: TEST-ENV-CLEANUP +name: Testing # Remove test instance for closed pull requests on: diff --git a/.github/workflows/test-env-deploy.yml b/.github/workflows/test-env-deploy.yml deleted file mode 100644 index ca0da6e0bb2..00000000000 --- a/.github/workflows/test-env-deploy.yml +++ /dev/null @@ -1,267 +0,0 @@ -name: TEST-ENV-DEPLOYMENT -# Build and deploy test instance for every pull request - -on: [pull_request] -jobs: - deploy: - if: github.event.pull_request.head.repo.full_name == 'saleor/saleor-dashboard' - runs-on: ubuntu-22.04 - outputs: - base_URL: ${{ steps.set-domain.outputs.domain }} - steps: - - uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version-file: ".nvmrc" - - - uses: rlespinasse/github-slug-action@v4 - - - name: Start deployment - uses: bobheadxi/deployments@v1 - id: deployment - with: - step: start - token: ${{ secrets.GITHUB_TOKEN }} - env: ${{ env.GITHUB_HEAD_REF_SLUG_URL }} - ref: ${{ github.head_ref }} - - - name: Start storybook deployment - uses: bobheadxi/deployments@v1 - id: storybook-deployment - with: - step: start - token: ${{ secrets.GITHUB_TOKEN }} - env: storybook ${{ env.GITHUB_HEAD_REF_SLUG_URL }} - ref: ${{ github.head_ref }} - - - name: Cache node modules - uses: actions/cache@v3 - env: - cache-name: cache-node-modules - with: - path: ~/.npm - key: ${{ runner.os }}-qa-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-qa-${{ env.cache-name }}- - ${{ runner.os }}-qa- - ${{ runner.os }}- - - name: Install deps - run: | - npm ci - - name: Get custom API_URI - id: api_uri - # Search for API_URI in PR description - env: - pull_request_body: ${{ github.event.pull_request.body }} - prefix: API_URI= - pattern: (http|https)://[a-zA-Z0-9.-]+/graphql/? - run: | - echo "custom_api_uri=$(echo "$pull_request_body" | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1)" >> $GITHUB_OUTPUT - - name: Get APPS_MARKETPLACE_API_URI - id: apps_marketplace_api_uri - # Search for APPS_MARKETPLACE_API_URI in PR description - env: - pull_request_body: ${{ github.event.pull_request.body }} - prefix: APPS_MARKETPLACE_API_URI= - pattern: (http|https)://[a-zA-Z0-9.-]+[a-zA-Z0-9/-]+/? - run: | - echo "custom_apps_marketplace_api_uri=$(echo "$pull_request_body" | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1)" >> $GITHUB_OUTPUT - - name: Run build - env: - # Use custom API_URI or the default one - API_URI: ${{ steps.api_uri.outputs.custom_api_uri || 'https://qa.staging.saleor.cloud/graphql/' }} - APPS_MARKETPLACE_API_URI: ${{ steps.apps_marketplace_api_uri.outputs.custom_apps_marketplace_api_uri }} - APP_MOUNT_URI: / - STATIC_URL: / - IS_CLOUD_INSTANCE: true - run: | - npm run build - - name: Run build storybook - run: | - npm run build-storybook - - name: Set domain - id: set-domain - # Set test instance domain based on branch name slug - run: | - echo "domain=${{ env.GITHUB_HEAD_REF_SLUG_URL }}.dashboard.saleor.rocks" >> $GITHUB_OUTPUT - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v3 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_DEFAULT_REGION }} - - - name: Deploy to S3 - run: | - aws s3 sync ./build/dashboard s3://${{ secrets.AWS_TEST_DEPLOYMENT_BUCKET }}/${{ steps.set-domain.outputs.domain }} - aws s3 sync ./build/storybook s3://${{ secrets.AWS_TEST_DEPLOYMENT_BUCKET }}/${{ steps.set-domain.outputs.domain }}/storybook - - name: Invalidate cache - run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_TEST_CF_DIST_ID }} --paths "/${{ steps.set-domain.outputs.domain }}/*" - - - name: Update deployment status - uses: bobheadxi/deployments@v1 - if: always() - with: - step: finish - token: ${{ secrets.GITHUB_TOKEN }} - status: ${{ job.status }} - env_url: https://${{ steps.set-domain.outputs.domain }}/ - deployment_id: ${{ steps.deployment.outputs.deployment_id }} - env: ${{ env.GITHUB_HEAD_REF_SLUG_URL }} - - - name: Update storybook deployment status - uses: bobheadxi/deployments@v1 - if: always() - with: - step: finish - token: ${{ secrets.GITHUB_TOKEN }} - status: ${{ job.status }} - env_url: https://${{ steps.set-domain.outputs.domain }}/storybook/index.html - deployment_id: ${{ steps.storybook-deployment.outputs.deployment_id }} - env: storybook ${{ env.GITHUB_HEAD_REF_SLUG_URL }} - - prepare-tests: - runs-on: ubuntu-22.04 - needs: deploy - outputs: - tags: ${{steps.get_tags.outputs.result}} - containers: ${{ steps.get_containers.outputs.result}} - steps: - - name: Get tags - id: get_tags - uses: actions/github-script@v6 - env: - pullRequestBody: ${{ github.event.pull_request.body }} - with: - result-encoding: string - script: | - const { pullRequestBody } = process.env - const tags = ["@critical"]; - try{ - const removedPullRequestBodyBeforeTests = pullRequestBody.split(`### Do you want to run more stable tests?`); - const removedPullRequestBodyAfterTests = removedPullRequestBodyBeforeTests[1].split(`CONTAINERS`); - let tagsInString = removedPullRequestBodyAfterTests[0]; - tagsInString = tagsInString.split('\n'); - tagsInString.forEach(line => { - if (line.includes('[x]')) tags.push(line.replace(/[0-9]+\. \[x\] /, "@stable+@")) - }); - const tagsToReturn = tags.join(",").toString(); - return tagsToReturn.replace(/\r/g, '') - }catch{ - return '@critical' - } - - - name: get-containers - id: get_containers - uses: actions/github-script@v6 - env: - pullRequestBody: ${{ github.event.pull_request.body }} - with: - script: | - const { pullRequestBody } = process.env - const containers = []; - const numberOfContainersRegex = /CONTAINERS=(\d*)/ - const numberOfContainers = pullRequestBody.match(numberOfContainersRegex); - for(let i=1; i<=numberOfContainers[1]; i++){ - containers.push(i) - } - return {"containers": containers} - - - name: echo-tags - run: | - echo ${{steps.get_tags.outputs.result}} - - testmo-report-preparation: - needs: prepare-tests - if: github.event.pull_request.head.repo.fork == false - runs-on: ubuntu-22.04 - outputs: - testmo-run-id: ${{ steps.init-testmo.outputs.testmo-run-id }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/testmo/testmo-init - with: - testmoUrl: ${{ secrets.TESTMO_URL }} - testmoToken: ${{ secrets.TESTMO_TOKEN }} - id: init-testmo - - cypress-run-selected: - runs-on: ubuntu-22.04 - needs: [prepare-tests, deploy, testmo-report-preparation] - container: cypress/browsers:node18.12.0-chrome106-ff106 - strategy: - fail-fast: false - max-parallel: 6 - matrix: ${{ fromJson(needs.prepare-tests.outputs.containers) }} - - steps: - - uses: actions/checkout@v4 - - name: Get API_URI - id: api_uri - # Search for API_URI in PR description and use default if not defined - env: - pull_request_body: ${{ github.event.pull_request.body }} - prefix: API_URI= - pattern: (http|https)://[a-zA-Z0-9.-]+/graphql/? - fallback_uri: ${{ secrets.CYPRESS_API_URI }} - run: | - echo "custom_api_uri=$(echo "$pull_request_body" | grep -Eo "$prefix$pattern" | sed s/$prefix// | head -n 1 | { read custom_uri; if [ -z "$custom_uri" ]; then echo "$fallback_uri"; else echo "$custom_uri"; fi })" >> $GITHUB_OUTPUT - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version-file: ".nvmrc" - - - name: Cache node modules - uses: actions/cache@v3 - env: - cache-name: cache-node-modules - with: - path: ~/.npm - key: ${{ runner.os }}-qa-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-qa-${{ env.cache-name }}- - ${{ runner.os }}-qa- - ${{ runner.os }}- - - name: Install Dependencies - if: steps.cache-node-modules.outputs.cache-hit != 'true' - run: NODE_OPTIONS=--max_old_space_size=4096 npm install - - - name: Cypress run critical - if: ${{ ! cancelled() }} - uses: ./.github/actions/e2e - with: - apiUrl: "${{ steps.api_uri.outputs.custom_api_uri }}" - appMountUri: ${{ secrets.APP_MOUNT_URI }} - baseUrl: "https://${{needs.deploy.outputs.base_URL}}/" - userName: ${{ secrets.CYPRESS_USER_NAME }} - secondUserName: ${{ secrets.CYPRESS_SECOND_USER_NAME }} - userPassword: ${{ secrets.CYPRESS_USER_PASSWORD }} - permissionsUserPassword: ${{ secrets.CYPRESS_PERMISSIONS_USERS_PASSWORD }} - mailpitUrl: ${{ secrets.CYPRESS_MAILPITURL }} - cypressGrepTags: ${{ needs.prepare-tests.outputs.tags }} - split: ${{ strategy.job-total }} - splitIndex: ${{ strategy.job-index }} - - name: Testmo threads submit - if: github.event.pull_request.head.repo.fork == false && !cancelled() - uses: ./.github/actions/testmo/testmo-threads-submit - with: - testmoUrl: ${{ secrets.TESTMO_URL }} - testmoToken: ${{ secrets.TESTMO_TOKEN }} - testmoRunId: ${{ needs.testmo-report-preparation.outputs.testmo-run-id }} - - test-complete: - needs: [testmo-report-preparation, cypress-run-selected] - if: | - always() && !contains(needs.*.result, 'skipped') && !contains(needs.*.result, 'cancelled') && github.event.pull_request.head.repo.fork == false - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - run: npm ci - working-directory: .github/workflows - - name: complete testmo report - uses: ./.github/actions/testmo/testmo-finish - with: - testmoUrl: ${{ secrets.TESTMO_URL }} - testmoToken: ${{ secrets.TESTMO_TOKEN }} - testmoRunId: ${{ needs.testmo-report-preparation.outputs.testmo-run-id }} diff --git a/.github/workflows/tests-nightly.yml b/.github/workflows/tests-nightly.yml index c9fda1fe255..4ec000db8dc 100644 --- a/.github/workflows/tests-nightly.yml +++ b/.github/workflows/tests-nightly.yml @@ -153,9 +153,9 @@ jobs: case 'workflow_dispatch': return browser case 'schedule': - return 'chrome' + return 'electron' default: - return 'chrome' + return 'electron' } - name: Cypress install @@ -167,7 +167,7 @@ jobs: - name: Cypress run electron id: cypress-electron - if: ${{ github.event.inputs.tests != 'Critical' && github.event_name != 'repository_dispatch' && contains(fromJSON('["chrome", "all"]'), steps.get-browsers.outputs.result) && ! cancelled() }} + if: ${{ github.event.inputs.tests != 'Critical' && github.event_name != 'repository_dispatch' && contains(fromJSON('["electron", "all"]'), steps.get-browsers.outputs.result) && ! cancelled() }} uses: ./.github/actions/e2e with: apiUrl: ${{ steps.get-env-uri.outputs.ENV_URI }}graphql/ @@ -211,7 +211,7 @@ jobs: install: false browser: firefox - name: Testmo threads submit - if: ${{ github.event.inputs.tests != 'Critical' && github.event_name != 'repository_dispatch' && contains(fromJSON('["chrome", "all"]'), steps.get-browsers.outputs.result) && ! cancelled() }} + if: ${{ github.event.inputs.tests != 'Critical' && github.event_name != 'repository_dispatch' && contains(fromJSON('["electron", "all"]'), steps.get-browsers.outputs.result) && ! cancelled() }} uses: ./.github/actions/testmo/testmo-threads-submit with: testmoUrl: ${{ secrets.TESTMO_URL }} diff --git a/.husky/pre-push b/.husky/pre-push index d0eecb18f1d..d43c508b19f 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,5 +1,15 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" +# Maximum allowed branch name length (deployment instance name limit) +MAX_LENGTH=37 + +# Check if the branch name length exceeds the maximum allowed length +branch_name=$(git symbolic-ref --short HEAD) +if [ ${#branch_name} -gt $MAX_LENGTH ]; then + echo "⚠️ Warning: Branch name '$branch_name' exceeds the maximum allowed length of $MAX_LENGTH characters." + echo "⚠️ The deployment instance will not be created." +fi + npm run check-types npm run test diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 8e77ec8b5e1..a2ec30fc7d4 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -9,6 +9,7 @@ API_URL: "", APP_MOUNT_URI: "/", IS_CLOUD_INSTANCE: false, + LOCALE_CODE: "EN", }; window.process = { cwd: () => "" }; diff --git a/Dockerfile b/Dockerfile index f8ba24b904c..50b69d18316 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ ARG APPS_MARKETPLACE_API_URI ARG APPS_TUNNEL_URL_KEYWORDS ARG STATIC_URL ARG SKIP_SOURCEMAPS +ARG LOCALE_CODE ENV API_URI ${API_URI:-http://localhost:8000/graphql/} ENV APP_MOUNT_URI ${APP_MOUNT_URI:-/dashboard/} @@ -33,6 +34,7 @@ ENV APPS_MARKETPLACE_API_URI ${APPS_MARKETPLACE_API_URI:-https://apps.saleor.io/ ENV APPS_TUNNEL_URL_KEYWORDS ${APPS_TUNNEL_URL_KEYWORDS} ENV STATIC_URL ${STATIC_URL:-/dashboard/} ENV SKIP_SOURCEMAPS ${SKIP_SOURCEMAPS:-true} +ENV LOCALE_CODE ${LOCALE_CODE:-EN} RUN npm run build FROM nginx:stable-alpine as runner diff --git a/app.json b/app.json index 4f47a67746a..21682107b5e 100644 --- a/app.json +++ b/app.json @@ -14,6 +14,10 @@ "APP_MOUNT_URI": { "description": "URI at which the Dashboard app will be mounted", "value": "/" + }, + "LOCALE_CODE": { + "description": "Locale code to select default language", + "value": "EN" } }, "buildpacks": [ diff --git a/cypress.config.js b/cypress.config.js index 384450108a5..a7a6a49ebdb 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -14,7 +14,7 @@ module.exports = defineConfig({ screenshotsFolder: "cypress/reports/mochareports", screenshotOnRunFailure: true, experimentalMemoryManagement: true, - numTestsKeptInMemory: 0, + numTestsKeptInMemory: 8, retries: { runMode: 2, openMode: 0, diff --git a/cypress/elements/discounts/vouchers.js b/cypress/elements/discounts/vouchers.js index 74fc77534d1..e4c0ec36c54 100644 --- a/cypress/elements/discounts/vouchers.js +++ b/cypress/elements/discounts/vouchers.js @@ -1,6 +1,9 @@ export const VOUCHERS_SELECTORS = { createVoucherButton: "[data-test-id='create-voucher']", - voucherCodeInput: "[name='code']", + manualVoucherItem: "[data-test-id='manual']", + voucherCodeConfirmButton: "[data-test-id='confirm-button']", + voucherCodeAddButton: "[data-test-id='add-code-button']", + voucherCodeNameInput: "[data-test-id='enter-code-input']", discountRadioButtons: "[name='discountType']", percentageDiscountRadioButton: "[name='discountType'][value='VALUE_PERCENTAGE']", @@ -17,12 +20,12 @@ export const VOUCHERS_SELECTORS = { usageLimitCheckbox: '[data-test-id="has-usage-limit"]', usageLimitTextField: '[data-test-id="usage-limit"]', applyOncePerCustomerCheckbox: '[data-test-id="apply-once-per-customer"]', - onlyForStaffCheckbox: '[data-test-id="only-for-staff"]' + onlyForStaffCheckbox: '[data-test-id="only-for-staff"]', }, requirements: { minOrderValueCheckbox: '[name="requirementsPicker"][value="ORDER"]', minAmountOfItemsCheckbox: '[name="requirementsPicker"][value="ITEM"]', minCheckoutItemsQuantityInput: '[name="minCheckoutItemsQuantity"]', - minOrderValueInput: '[name="minSpent"]' - } + minOrderValueInput: '[name="minSpent"]', + }, }; diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 1c73490b057..ab6693952bb 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -38,6 +38,7 @@ module.exports = async (on, config) => { config.env.MAILPITURL = process.env.CYPRESS_MAILPITURL; config.env.grepTags = process.env.CYPRESS_grepTags; config.baseUrl = process.env.BASE_URL; + config.env.LOCALE_CODE = process.env.LOCALE_CODE; on("before:browser:launch", (_browser = {}, launchOptions) => { launchOptions.args.push("--proxy-bypass-list=<-loopback>"); diff --git a/cypress/support/customCommands/user/index.js b/cypress/support/customCommands/user/index.js index 0e62fd78cc6..18717a28c6d 100644 --- a/cypress/support/customCommands/user/index.js +++ b/cypress/support/customCommands/user/index.js @@ -19,10 +19,10 @@ Cypress.Commands.add("loginInShop", () => { }); Cypress.Commands.add("visitHomePageLoggedViaApi", user => { - cy.addAliasToGraphRequest("Home") + cy.addAliasToGraphRequest("UserDetails") .loginUserViaRequest("auth", user) .visit(urlList.homePage) - .waitForRequestAndCheckIfNoErrors("@Home"); + .waitForRequestAndCheckIfNoErrors("@UserDetails"); }); Cypress.Commands.add( diff --git a/cypress/support/pages/discounts/vouchersPage.js b/cypress/support/pages/discounts/vouchersPage.js index 8728da31ff0..1bd90b9c0e4 100644 --- a/cypress/support/pages/discounts/vouchersPage.js +++ b/cypress/support/pages/discounts/vouchersPage.js @@ -25,10 +25,11 @@ export function createVoucher({ }) { cy.get(VOUCHERS_SELECTORS.createVoucherButton).click(); selectChannelInDetailsPages(channelName); - cy.get(VOUCHERS_SELECTORS.voucherCodeInput) - .type(voucherCode) - .get(discountOption) - .click(); + cy.get(VOUCHERS_SELECTORS.voucherCodeAddButton).click(); + cy.get(VOUCHERS_SELECTORS.manualVoucherItem).click(); + cy.get(VOUCHERS_SELECTORS.voucherCodeNameInput).type(voucherCode); + cy.get(VOUCHERS_SELECTORS.voucherCodeConfirmButton).click(); + cy.get(discountOption).click(); if (discountOption !== discountOptions.SHIPPING) { cy.get(VOUCHERS_SELECTORS.discountValueInputs).type(voucherValue, { force: true, diff --git a/cypress/support/pages/homePage.js b/cypress/support/pages/homePage.js index 09abf4a0cae..f501eafa5e6 100644 --- a/cypress/support/pages/homePage.js +++ b/cypress/support/pages/homePage.js @@ -4,11 +4,9 @@ import { HOMEPAGE_SELECTORS } from "../../elements/homePage/homePage-selectors"; export function changeChannel(channelName) { cy.get(HEADER_SELECTORS.channelSelect) .click() - .addAliasToGraphRequest("Home") .get(HEADER_SELECTORS.channelSelectList) .contains(channelName) - .click() - .wait("@Home"); + .click(); } export function expectWelcomeMessageIncludes(name) { @@ -21,37 +19,37 @@ export function expectWelcomeMessageIncludes(name) { export function getOrdersReadyToFulfillRegex( ordersReadyToFulfillBefore, - quantityOfNewOrders + quantityOfNewOrders, ) { const allOrdersReadyToFulfill = ordersReadyToFulfillBefore + quantityOfNewOrders; const notANumberRegex = "\\D*"; return new RegExp( - `${notANumberRegex}${allOrdersReadyToFulfill}${notANumberRegex}` + `${notANumberRegex}${allOrdersReadyToFulfill}${notANumberRegex}`, ); } export function getOrdersReadyForCaptureRegex( ordersReadyForCaptureBefore, - quantityOfNewOrders + quantityOfNewOrders, ) { const allOrdersReadyForCapture = ordersReadyForCaptureBefore + quantityOfNewOrders; const notANumberRegex = "\\D*"; return new RegExp( - `${notANumberRegex}${allOrdersReadyForCapture}${notANumberRegex}` + `${notANumberRegex}${allOrdersReadyForCapture}${notANumberRegex}`, ); } export function getProductsOutOfStockRegex( productsOutOfStockBefore, - quantityOfNewProducts + quantityOfNewProducts, ) { const allProductsOutOfStock = productsOutOfStockBefore + quantityOfNewProducts; const notANumberRegex = "\\D*"; return new RegExp( - `${notANumberRegex}${allProductsOutOfStock}${notANumberRegex}` + `${notANumberRegex}${allProductsOutOfStock}${notANumberRegex}`, ); } @@ -67,7 +65,7 @@ export function getSalesAmountRegex(salesAmountBefore, addedAmount) { const totalAmountWithSeparators = `${totalAmountIntegerWithThousandsSeparator}${decimalSeparator}${totalAmountDecimalValue}`; const notANumberRegex = "\\D*"; return new RegExp( - `${notANumberRegex}${totalAmountWithSeparators}${notANumberRegex}` + `${notANumberRegex}${totalAmountWithSeparators}${notANumberRegex}`, ); } diff --git a/introspection.json b/introspection.json index 527d79c8d67..832687c9be0 100644 --- a/introspection.json +++ b/introspection.json @@ -17703,7 +17703,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether checkout prices should include taxes when displayed in a storefront.\n\nAdded in Saleor 3.9.", + "description": "Determines whether displayed prices should include taxes.\n\nAdded in Saleor 3.9.", "args": [], "type": { "kind": "NON_NULL", @@ -18314,6 +18314,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "voucher", + "description": "The voucher assigned to the checkout.\n\nAdded in Saleor 3.18.\n\nRequires one of the following permissions: MANAGE_DISCOUNTS.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Voucher", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "voucherCode", "description": "The code of voucher assigned to the checkout.", @@ -29869,6 +29881,26 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "voucherCodes", + "description": "List of voucher codes which causes the error.\n\nAdded in Saleor 3.18.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -29931,6 +29963,12 @@ "description": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "VOUCHER_ALREADY_USED", + "description": null, + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null @@ -30049,7 +30087,7 @@ { "kind": "OBJECT", "name": "Domain", - "description": "Represents shop's domain.", + "description": "Represents API domain.", "fields": [ { "name": "host", @@ -30085,7 +30123,7 @@ }, { "name": "url", - "description": "Shop's absolute URL.", + "description": "The absolute URL of the API.", "args": [], "type": { "kind": "NON_NULL", @@ -30479,6 +30517,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "voucherCode", + "description": "A code of the voucher associated with the order.\n\nAdded in Saleor 3.18.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -30847,6 +30897,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "voucherCode", + "description": "A code of the voucher associated with the order.\n\nAdded in Saleor 3.18.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -31908,6 +31970,11 @@ "name": "TranslationUpdated", "ofType": null }, + { + "kind": "OBJECT", + "name": "VoucherCodeExportCompleted", + "ofType": null + }, { "kind": "OBJECT", "name": "VoucherCreated", @@ -33743,6 +33810,112 @@ ], "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "ExportVoucherCodes", + "description": "Export voucher codes to csv/xlsx file.\n\nAdded in Saleor 3.18.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_DISCOUNTS.\n\nTriggers the following webhook events:\n- VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file.", + "fields": [ + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ExportError", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "exportFile", + "description": "The newly created export file job which is responsible for export data.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ExportFile", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "ExportVoucherCodesInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "fileType", + "description": "Type of exported file.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "FileTypesEnum", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ids", + "description": "List of voucher code IDs to export.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "voucherId", + "description": "The ID of the voucher. If provided, exports all codes belonging to the voucher.", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "ExternalAuthentication", @@ -34809,6 +34982,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "shippingRefundedAmount", + "description": "Amount of refunded shipping price.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Money", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "status", "description": null, @@ -34837,6 +35022,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "totalRefundedAmount", + "description": "Total refunded amount assigned to this fulfillment.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "Money", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "trackingNumber", "description": null, @@ -54804,6 +55001,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "exportVoucherCodes", + "description": "Export voucher codes to csv/xlsx file.\n\nAdded in Saleor 3.18.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_DISCOUNTS.\n\nTriggers the following webhook events:\n- VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file.", + "args": [ + { + "name": "input", + "description": "Fields required to export voucher codes.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "ExportVoucherCodesInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ExportVoucherCodes", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "externalAuthenticationUrl", "description": "Prepare external authentication URL for user by custom plugin.", @@ -62390,7 +62616,7 @@ }, { "name": "stockBulkUpdate", - "description": "Updates stocks for a given variant and warehouse.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_PRODUCTS.", + "description": "Updates stocks for a given variant and warehouse.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_PRODUCTS.\n\nTriggers the following webhook events:\n- PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated.", "args": [ { "name": "errorPolicy", @@ -63088,7 +63314,7 @@ "args": [ { "name": "action", - "description": "The expected action called for the transaction. By default, the `channel.defaultTransactionFlowStrategy` will be used. The field can be used only by app that has `HANDLE_PAYMENTS` permission.", + "description": "The expected action called for the transaction. By default, the `channel.paymentSettings.defaultTransactionFlowStrategy` will be used.The field can be used only by app that has `HANDLE_PAYMENTS` permission.", "type": { "kind": "ENUM", "name": "TransactionFlowStrategyEnum", @@ -63939,6 +64165,43 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "voucherCodeBulkDelete", + "description": "Deletes voucher codes.\n\nAdded in Saleor 3.18. \n\nRequires one of the following permissions: MANAGE_DISCOUNTS.\n\nTriggers the following webhook events:\n- VOUCHER_UPDATED (async): A voucher was updated.", + "args": [ + { + "name": "ids", + "description": "List of voucher codes IDs to delete.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "VoucherCodeBulkDelete", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "voucherCreate", "description": "Creates a new voucher. \n\nRequires one of the following permissions: MANAGE_DISCOUNTS.\n\nTriggers the following webhook events:\n- VOUCHER_CREATED (async): A voucher was created.", @@ -65492,7 +65755,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether checkout prices should include taxes when displayed in a storefront.\n\nAdded in Saleor 3.9.", + "description": "Determines whether displayed prices should include taxes.\n\nAdded in Saleor 3.9.", "args": [], "type": { "kind": "NON_NULL", @@ -66622,6 +66885,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "voucherCode", + "description": "Voucher code that was used for Order.\n\nAdded in Saleor 3.18.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "weight", "description": null, @@ -67546,7 +67821,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether checkout prices should include taxes, when displayed in a storefront.", + "description": "Determines whether displayed prices should include taxes.", "type": { "kind": "SCALAR", "name": "Boolean", @@ -67802,7 +68077,19 @@ }, { "name": "voucher", - "description": "Code of a voucher associated with the order.", + "description": "Code of a voucher associated with the order.\n\nDEPRECATED: this field will be removed in Saleor 3.19. Use `voucherCode` instead.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "voucherCode", + "description": "Code of a voucher associated with the order.\n\nAdded in Saleor 3.18.", "type": { "kind": "SCALAR", "name": "String", @@ -70088,6 +70375,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "INVALID_VOUCHER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INVALID_VOUCHER_CODE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "NOT_AVAILABLE_IN_CHANNEL", "description": null, @@ -73479,6 +73778,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "saleId", + "description": "Denormalized sale ID, set when order line is created for a product variant that is on sale.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "taxClass", "description": "Denormalized tax class of the product in this order line.\n\nAdded in Saleor 3.9.\n\nRequires one of the following permissions: AUTHENTICATED_STAFF_USER, AUTHENTICATED_APP.", @@ -73783,6 +74094,18 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "voucherCode", + "description": "Voucher code that was used for this order line.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -75449,22 +75772,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "defaultTransactionFlowStrategy", - "description": "Determine the transaction flow strategy to be used. Include the selected option in the payload sent to the payment app, as a requested action for the transaction.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.This preview feature field will be removed in Saleor 3.17. Use `PaymentSettings.defaultTransactionFlowStrategy` instead.", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "ENUM", - "name": "TransactionFlowStrategyEnum", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "deleteExpiredOrdersAfter", "description": "The time in days after expired orders will be deleted.\n\nAdded in Saleor 3.14.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", @@ -75493,6 +75800,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "includeDraftOrderInVoucherUsage", + "description": "Determine if voucher applied on draft order should be count toward voucher usage.\n\nAdded in Saleor 3.18.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "markAsPaidStrategy", "description": "Determine what strategy will be used to mark the order as paid. Based on the chosen option, the proper object will be created and attached to the order when it's manually marked as paid.\n`PAYMENT_FLOW` - [default option] creates the `Payment` object.\n`TRANSACTION_FLOW` - creates the `TransactionItem` object.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", @@ -75626,11 +75949,11 @@ "deprecationReason": null }, { - "name": "defaultTransactionFlowStrategy", - "description": "Determine the transaction flow strategy to be used. Include the selected option in the payload sent to the payment app, as a requested action for the transaction.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.\n\nDEPRECATED: this preview feature field will be removed in Saleor 3.17. Use `PaymentSettingsInput.defaultTransactionFlowStrategy` instead.", + "name": "deleteExpiredOrdersAfter", + "description": "The time in days after expired orders will be deleted.Allowed range is from 1 to 120.\n\nAdded in Saleor 3.14.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", "type": { - "kind": "ENUM", - "name": "TransactionFlowStrategyEnum", + "kind": "SCALAR", + "name": "Day", "ofType": null }, "defaultValue": null, @@ -75638,11 +75961,11 @@ "deprecationReason": null }, { - "name": "deleteExpiredOrdersAfter", - "description": "The time in days after expired orders will be deleted.Allowed range is from 1 to 120.\n\nAdded in Saleor 3.14.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", + "name": "expireOrdersAfter", + "description": "Expiration time in minutes. Default null - means do not expire any orders. Enter 0 or null to disable.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", "type": { "kind": "SCALAR", - "name": "Day", + "name": "Minute", "ofType": null }, "defaultValue": null, @@ -75650,11 +75973,11 @@ "deprecationReason": null }, { - "name": "expireOrdersAfter", - "description": "Expiration time in minutes. Default null - means do not expire any orders. Enter 0 or null to disable.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", + "name": "includeDraftOrderInVoucherUsage", + "description": "Specify whether a coupon applied to draft orders will count toward voucher usage.\n\nWarning: when switching this setting from `false` to `true`, the vouchers will be disconnected from all draft orders.\n\nAdded in Saleor 3.18.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", "type": { "kind": "SCALAR", - "name": "Minute", + "name": "Boolean", "ofType": null }, "defaultValue": null, @@ -75769,7 +76092,7 @@ }, { "name": "automaticallyFulfillNonShippableGiftCard", - "description": "When enabled, all non-shippable gift card orders will be fulfilled automatically. By defualt set to True.", + "description": "When enabled, all non-shippable gift card orders will be fulfilled automatically. By default set to True.", "type": { "kind": "SCALAR", "name": "Boolean", @@ -80425,6 +80748,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "partial", + "description": "Informs whether this is a partial payment.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "paymentMethodType", "description": "Type of method used for payment.", @@ -80527,6 +80866,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "pspReference", + "description": "PSP reference of the payment.\n\nAdded in Saleor 3.14.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "token", "description": "Unique token associated with a payment.", @@ -91714,7 +92065,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether this product's price displayed in a storefront should include taxes.\n\nAdded in Saleor 3.9.", + "description": "Determines whether displayed prices should include taxes.\n\nAdded in Saleor 3.9.", "args": [], "type": { "kind": "NON_NULL", @@ -118724,7 +119075,7 @@ { "kind": "OBJECT", "name": "StockBulkUpdate", - "description": "Updates stocks for a given variant and warehouse.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_PRODUCTS.", + "description": "Updates stocks for a given variant and warehouse.\n\nAdded in Saleor 3.13.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point. \n\nRequires one of the following permissions: MANAGE_PRODUCTS.\n\nTriggers the following webhook events:\n- PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated.", "fields": [ { "name": "count", @@ -121043,7 +121394,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether prices displayed in a storefront should include taxes.", + "description": "Determines whether displayed prices should include taxes.", "args": [], "type": { "kind": "NON_NULL", @@ -121486,7 +121837,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether prices displayed in a storefront should include taxes for this country.", + "description": "Determines whether displayed prices should include taxes for this country.", "args": [], "type": { "kind": "NON_NULL", @@ -121558,7 +121909,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether prices displayed in a storefront should include taxes for this country.", + "description": "Determines whether displayed prices should include taxes for this country.", "type": { "kind": "NON_NULL", "name": null, @@ -121766,7 +122117,7 @@ }, { "name": "displayGrossPrices", - "description": "Determines whether prices displayed in a storefront should include taxes.", + "description": "Determines whether displayed prices should include taxes.", "type": { "kind": "SCALAR", "name": "Boolean", @@ -129631,16 +129982,73 @@ }, { "name": "code", - "description": "The code of the voucher.", + "description": "The code of the voucher.This field will be removed in Saleor 4.0.", "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "codes", + "description": "List of codes available for this voucher.\n\nAdded in Saleor 3.18.", + "args": [ + { + "name": "after", + "description": "Return the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Return the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Retrieve the first n elements from the list. Note that the system only allows fetching a maximum of 100 objects in a single query.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Retrieve the last n elements from the list. Note that the system only allows fetching a maximum of 100 objects in a single query.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } + ], + "type": { + "kind": "OBJECT", + "name": "VoucherCodeCountableConnection", + "ofType": null }, "isDeprecated": false, "deprecationReason": null @@ -130079,6 +130487,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "singleUse", + "description": "Determine if the voucher codes can be used once or multiple times.\n\nAdded in Saleor 3.18.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "startDate", "description": "The start date and time of voucher.", @@ -130658,6 +131082,419 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "VoucherCode", + "description": "Represents voucher code.\n\nAdded in Saleor 3.18.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", + "fields": [ + { + "name": "code", + "description": "Code to use the voucher.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Date time of code creation.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "The ID of the voucher code.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isActive", + "description": "Whether a code is active or not.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "used", + "description": "Number of times a code has been used.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VoucherCodeBulkDelete", + "description": "Deletes voucher codes.\n\nAdded in Saleor 3.18. \n\nRequires one of the following permissions: MANAGE_DISCOUNTS.\n\nTriggers the following webhook events:\n- VOUCHER_UPDATED (async): A voucher was updated.", + "fields": [ + { + "name": "count", + "description": "Returns how many codes were deleted.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "VoucherCodeBulkDeleteError", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VoucherCodeBulkDeleteError", + "description": null, + "fields": [ + { + "name": "code", + "description": "The error code.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "VoucherCodeBulkDeleteErrorCode", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "The error message.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "path", + "description": "Path to field that caused the error. A value of `null` indicates that the error isn't associated with a particular field.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "voucherCodes", + "description": "List of voucher codes which causes the error.", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "VoucherCodeBulkDeleteErrorCode", + "description": "An enumeration.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "GRAPHQL_ERROR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INVALID", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NOT_FOUND", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VoucherCodeCountableConnection", + "description": null, + "fields": [ + { + "name": "edges", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "VoucherCodeCountableEdge", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Pagination data for this connection.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "A total count of items in the collection.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VoucherCodeCountableEdge", + "description": null, + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "VoucherCode", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "VoucherCodeExportCompleted", + "description": "Event sent when voucher code export is completed.\n\nAdded in Saleor 3.18.", + "fields": [ + { + "name": "export", + "description": "The export file for voucher codes.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "ExportFile", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issuedAt", + "description": "Time of the event.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issuingPrincipal", + "description": "The user or application that triggered the event.", + "args": [], + "type": { + "kind": "UNION", + "name": "IssuingPrincipal", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recipient", + "description": "The application receiving the webhook.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "App", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "version", + "description": "Saleor version that triggered the event.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Event", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "VoucherCountableConnection", @@ -131248,6 +132085,26 @@ "description": null, "fields": null, "inputFields": [ + { + "name": "addCodes", + "description": "List of codes to add.\n\nAdded in Saleor 3.18.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "applyOncePerCustomer", "description": "Voucher should be applied once per customer.", @@ -131294,7 +132151,7 @@ }, { "name": "code", - "description": "Code to use the voucher.", + "description": "Code to use the voucher. This field will be removed in Saleor 4.0. Use `addCodes` instead.", "type": { "kind": "SCALAR", "name": "String", @@ -131424,6 +132281,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "singleUse", + "description": "When set to 'True', each voucher code can be used only once; otherwise, codes can be used multiple times depending on `usageLimit`.\n\nThe option can only be changed if none of the voucher codes have been used.\n\nAdded in Saleor 3.18.\n\nNote: this API is currently in Feature Preview and can be subject to changes at later point.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "startDate", "description": "Start date of the voucher in ISO 8601 format.", @@ -131656,7 +132525,7 @@ "enumValues": [ { "name": "CODE", - "description": "Sort vouchers by code.", + "description": "Sort vouchers by code.\n\nDEPRECATED: this field will be removed in Saleor 4.0.", "isDeprecated": false, "deprecationReason": null }, @@ -131672,6 +132541,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "NAME", + "description": "Sort vouchers by name.\n\nAdded in Saleor 3.18.", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "START_DATE", "description": "Sort vouchers by start date.", @@ -135465,7 +136340,7 @@ }, { "name": "PRODUCT_VARIANT_DELETED", - "description": "A product variant is deleted.", + "description": "A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED.", "isDeprecated": false, "deprecationReason": null }, @@ -135661,6 +136536,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "VOUCHER_CODE_EXPORT_COMPLETED", + "description": "A voucher code export is completed.\n\nAdded in Saleor 3.18.", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "VOUCHER_CREATED", "description": "A new voucher created.", @@ -136406,7 +137287,7 @@ }, { "name": "PRODUCT_VARIANT_DELETED", - "description": "A product variant is deleted.", + "description": "A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED.", "isDeprecated": false, "deprecationReason": null }, @@ -136644,6 +137525,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "VOUCHER_CODE_EXPORT_COMPLETED", + "description": "A voucher code export is completed.\n\nAdded in Saleor 3.18.", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "VOUCHER_CREATED", "description": "A new voucher created.", @@ -137632,6 +138519,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "VOUCHER_CODE_EXPORT_COMPLETED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "VOUCHER_CREATED", "description": null, diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 9234151da3f..a0468ef3faa 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -214,6 +214,9 @@ "context": "section description", "string": "Strategy defines the preference of warehouses for stock allocations and reservations." }, + "/Fa+RP": { + "string": "Couldn't load top products" + }, "/ILyIf": { "context": "tax classes menu header", "string": "Tax class label" @@ -241,6 +244,9 @@ "context": "page label", "string": "Hidden" }, + "/U8FUp": { + "string": "Couldn't load activities" + }, "/V7UOC": { "context": "unassign category from sale and save, button", "string": "Unassign and save" @@ -691,6 +697,9 @@ "context": "window title", "string": "Install App" }, + "2dgbGR": { + "string": "Those codes already exist" + }, "2fgaAQ": { "context": "search input placeholder", "string": "Search shipping zones..." @@ -974,6 +983,9 @@ "context": "Charge in progress transaction amount, data display header", "string": "Pending charge" }, + "4gJAm6": { + "string": "Can't delete saved codes" + }, "4gT3eD": { "context": "navigator section header", "string": "Search in Customers" @@ -1513,9 +1525,6 @@ "context": "button", "string": "Go back to dashboard" }, - "989O5D": { - "string": "Are you sure you want to delete this voucher code?" - }, "98Nw4g": { "context": "card subtitle", "string": "Rendered prices" @@ -1528,6 +1537,9 @@ "context": "tile view pagination label", "string": "No. of products" }, + "9BvYb9": { + "string": "Voucher" + }, "9C7PZE": { "context": "navigation section name", "string": "Navigation" @@ -1855,16 +1867,16 @@ "context": "page type name", "string": "Content Type Name" }, + "BR8au7": { + "context": "delete channel", + "string": "Select channel that you wish to move existing orders to." + }, "BUKMzM": { "string": "Variant removed" }, "BWpuKl": { "string": "Update" }, - "BXMSl4": { - "context": "currency channel", - "string": "There is no available channel to move order information to. Please create a channel with same currency so that information can be moved to it." - }, "BXkF8Z": { "context": "header", "string": "Activity" @@ -2552,6 +2564,9 @@ "context": "tooltip", "string": "Checkout reservation time threshold is enabled in settings." }, + "GA+Djy": { + "string": "Are you sure you want to delete these voucher codes?" + }, "GAmGog": { "context": "value input label", "string": "Discount value" @@ -2560,6 +2575,10 @@ "context": "ProductTypeDeleteWarningDialog single assigned items button label", "string": "View products" }, + "GCho9N": { + "context": "delete channel", + "string": "All channel settings information such as shipping, product listings, warehouse assignments, etc, will be lost." + }, "GD/bom": { "context": "label", "string": "Min Delivery Time" @@ -2591,6 +2610,16 @@ "context": "webhooks inactive label", "string": "Inactive" }, + "GOdq5V": { + "string": "Catalog" + }, + "GP0zGO": { + "context": "dialog header", + "string": "Select destination channel:" + }, + "GTCg9O": { + "string": "You must add at least one voucher code" + }, "GVM/fi": { "context": "order history message", "string": "Payment was authorized" @@ -2619,6 +2648,10 @@ "context": "aria-label, negative money amount", "string": "minus" }, + "Ge+dUe": { + "context": "currency channel", + "string": "To delete {channelSlug} you have to create a chanel with currency: {currency} to be able to move all existing orders." + }, "Gfbp36": { "context": "dialog header", "string": "Unassign Products From Shipping" @@ -3136,10 +3169,6 @@ "JqiqNj": { "string": "Something went wrong" }, - "JsPIOX": { - "context": "voucher code", - "string": "Code" - }, "Jsh6+U": { "context": "product type is digital or physical", "string": "Type" @@ -3284,6 +3313,10 @@ "context": "channel select label", "string": "Channel" }, + "LATHyi": { + "context": "dialog header", + "string": "Delete Channel: {channelSlug}" + }, "LEZZkK": { "context": "WarehouseSettings all warehouses description", "string": "If selected customer will be able to choose this warehouse as pickup point. Ordered products can be shipped here from a different warehouse" @@ -3499,10 +3532,6 @@ "context": "add discount button", "string": "Add Discount" }, - "Mz0cx+": { - "context": "delete channel", - "string": "Deleting channel will delete all product data regarding this channel. Are you sure you want to delete this channel?" - }, "N2SbNc": { "string": "{counter,plural,one{Are you sure you want to delete this customer?} other{Are you sure you want to delete {displayQuantity} customers?}}" }, @@ -3919,6 +3948,9 @@ "context": "page header", "string": "Create Voucher" }, + "PuQb0P": { + "string": "Reward" + }, "Pyjarj": { "string": "This shipping rate has no postal codes assigned" }, @@ -4045,10 +4077,6 @@ "context": "button, unassign attribute from object", "string": "Unassign and save" }, - "QZoU0r": { - "context": "dialog header", - "string": "Delete Channel" - }, "Qb2XN5": { "string": "\"Mark as paid\" feature creates a {link} - used by Payment Apps" }, @@ -4268,6 +4296,9 @@ "context": "unassign attribute from product type, button", "string": "Unassign" }, + "S8kqP9": { + "string": "Conditions" + }, "SBb6Ej": { "context": "select a warehouse to fulfill product from", "string": "Select warehouse..." @@ -4331,10 +4362,6 @@ "context": "set variant as default, button", "string": "Set as default" }, - "SZJhvK": { - "context": "dialog header", - "string": "Select Channel" - }, "SZt9kC": { "context": "export filtered items to csv file", "string": "Current search ({number})" @@ -4885,6 +4912,9 @@ "context": "used by filter label", "string": "Used by" }, + "WMN0q+": { + "string": "Delete voucher codes" + }, "WQMTKI": { "context": "list of warehouses", "string": "Warehouses A to Z" @@ -4916,6 +4946,9 @@ "context": "dialog title", "string": "Delete attribute value" }, + "WY3IXU": { + "string": "This code already exists" + }, "WasHjQ": { "context": "voucher discount", "string": "Shipment" @@ -5132,6 +5165,9 @@ "Xtd0AT": { "string": "Original String" }, + "XtlUj6": { + "string": "Add your first rule to set up a promotion" + }, "Xu4ech": { "context": "deactivate app", "string": "Are you sure you want to disable this app? Your data will be kept until you reactivate the app." @@ -5489,6 +5525,10 @@ "aKSUWR": { "string": "Cannot create transaction to non-existing order" }, + "aMWJ7/": { + "context": "local error", + "string": "This field cannot be blank for a new webhook" + }, "aMwxYb": { "string": "Countries" }, @@ -5926,6 +5966,9 @@ "context": "search box placeholder", "string": "Search by country name" }, + "dHAwu8": { + "string": "Code already exists" + }, "dJQxHt": { "context": "delete category", "string": "Are you sure you want to delete {categoryName}?" @@ -6371,6 +6414,9 @@ "context": "PluginChannelConfigurationCell channel title", "string": "Per channel" }, + "gzM1em": { + "string": "Add rule" + }, "h1rPPg": { "context": "dialog content", "string": "Are you sure you want to delete {attributeName}?" @@ -6549,9 +6595,6 @@ "context": "Transaction cancel button - return preauthorized amount to client", "string": "Cancel" }, - "iL/zeh": { - "string": "Delete voucher code" - }, "iMJka8": { "string": "No pages found" }, @@ -6739,9 +6782,6 @@ "context": "dialog header", "string": "Assign product" }, - "jvKNMP": { - "string": "Discount Code" - }, "jvo0vs": { "string": "Save" }, @@ -6794,6 +6834,9 @@ "context": "total order price", "string": "Total" }, + "kAAlGL": { + "string": "Rules" + }, "kAPaN6": { "context": "empty metadata text", "string": "Empty" @@ -6842,6 +6885,9 @@ "kN6SLs": { "string": "Min Value" }, + "kNK4es": { + "string": "Discount value" + }, "kPIZ65": { "context": "page header", "string": "Order #{orderNumber}" @@ -7128,10 +7174,6 @@ "context": "Webhook details asynchronous events", "string": "Asynchronous" }, - "mSLr9d": { - "context": "voucher code, button", - "string": "Generate Code" - }, "mTEqYL": { "context": "NoChannels content", "string": "No channels to assign. Please first assign them for the product." @@ -7373,6 +7415,9 @@ "context": "Add filter button text", "string": "+ Add filter" }, + "o/4OCR": { + "string": "Shipping has already been refunded" + }, "o5KXAN": { "context": "delete webhook", "string": "Are you sure you want to delete {name}?" @@ -7973,10 +8018,6 @@ "context": "no products placeholder", "string": "No products are available in the channel assigned to this order." }, - "sidKce": { - "context": "delete channel", - "string": "All order information from this channel need to be moved to a different channel. Please select channel orders need to be moved to:." - }, "sjRXXz": { "string": "Order #{orderId} was placed from draft by {userEmail}" }, @@ -8227,6 +8268,9 @@ "context": "order history message", "string": "Order was confirmed" }, + "ucLtY8": { + "string": "Discount rules for products, collections or categories." + }, "uccjUM": { "context": "Dry run objects", "string": "Objects" @@ -8538,6 +8582,10 @@ "wWTUrM": { "string": "No activities found" }, + "wXFttp": { + "context": "note on currency", + "string": "Note: Only channels with matching currency are available." + }, "wbsq7O": { "string": "Usage" }, diff --git a/locale/pt_BR.json b/locale/pt-BR.json similarity index 100% rename from locale/pt_BR.json rename to locale/pt-BR.json diff --git a/package-lock.json b/package-lock.json index 418930176c7..2a7dc5c446c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,18 @@ { "name": "saleor-dashboard", - "version": "3.18.0-dev", + "version": "3.19.0-dev", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "saleor-dashboard", - "version": "3.18.0-dev", - "hasInstallScript": true, + "version": "3.19.0-dev", "license": "BSD-3-Clause", "dependencies": { "@apollo/client": "3.4.17", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@editorjs/editorjs": "^2.24.3", "@editorjs/header": "^2.6.2", "@editorjs/image": "^2.6.2", @@ -84,7 +86,6 @@ "react-router": "^5.0.1", "react-router-dom": "^5.0.1", "react-sortable-hoc": "^1.10.1", - "react-sortable-tree": "^2.6.2", "remark-gfm": "^3.0.1", "slugify": "^1.4.6", "tslib": "^2.4.1", @@ -139,7 +140,6 @@ "@types/react-infinite-scroller": "^1.2.3", "@types/react-router-dom": "^4.3.4", "@types/react-sortable-hoc": "^0.7.1", - "@types/react-sortable-tree": "^0.3.15", "@types/url-join": "^4.0.1", "@types/uuid": "^9.0.4", "@types/webappsec-credential-management": "^0.5.1", @@ -3048,6 +3048,55 @@ "node": ">=10.0.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz", + "integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz", + "integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==", + "dependencies": { + "@dnd-kit/accessibility": "^3.0.0", + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.7", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz", + "integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@editorjs/editorjs": { "version": "2.24.3", "license": "Apache-2.0", @@ -8790,18 +8839,6 @@ "react-dom": "^16.8.0 || 17.x" } }, - "node_modules/@react-dnd/asap": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/@react-dnd/invariant": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "2.0.0", - "license": "MIT" - }, "node_modules/@react-editor-js/client": { "version": "2.0.6", "dependencies": { @@ -20161,16 +20198,6 @@ "react-sortable-hoc": "*" } }, - "node_modules/@types/react-sortable-tree": { - "version": "0.3.15", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*", - "@types/react-virtualized": "*", - "react-dnd": "^11.1.3" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.1", "license": "MIT", @@ -20178,15 +20205,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-virtualized": { - "version": "9.21.21", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "@types/react": "^17" - } - }, "node_modules/@types/react/node_modules/csstype": { "version": "3.1.1", "license": "MIT" @@ -25075,15 +25093,6 @@ "node": ">=8" } }, - "node_modules/dnd-core": { - "version": "11.1.3", - "license": "MIT", - "dependencies": { - "@react-dnd/asap": "^4.0.0", - "@react-dnd/invariant": "^2.0.0", - "redux": "^4.0.4" - } - }, "node_modules/doctrine": { "version": "3.0.0", "devOptional": true, @@ -28743,47 +28752,6 @@ "js-yaml": "^3.13.1" } }, - "node_modules/frontend-collective-react-dnd-scrollzone": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "hoist-non-react-statics": "^3.1.0", - "lodash.throttle": "^4.0.1", - "prop-types": "^15.5.9", - "raf": "^3.2.0", - "react": "^16.3.0", - "react-display-name": "^0.2.0", - "react-dom": "^16.3.0" - }, - "peerDependencies": { - "react-dnd": "^7.3.0" - } - }, - "node_modules/frontend-collective-react-dnd-scrollzone/node_modules/react": { - "version": "16.14.0", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/frontend-collective-react-dnd-scrollzone/node_modules/react-dom": { - "version": "16.14.0", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - }, - "peerDependencies": { - "react": "^16.14.0" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -33560,7 +33528,8 @@ }, "node_modules/lodash.isequal": { "version": "4.5.0", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/lodash.isfunction": { "version": "3.0.9", @@ -33633,10 +33602,6 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "license": "MIT" - }, "node_modules/lodash.topairs": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", @@ -36757,7 +36722,8 @@ }, "node_modules/performance-now": { "version": "2.1.0", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/picocolors": { "version": "1.0.0", @@ -37550,13 +37516,6 @@ "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", "dev": true }, - "node_modules/raf": { - "version": "3.4.1", - "license": "MIT", - "dependencies": { - "performance-now": "^2.1.0" - } - }, "node_modules/ramda": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", @@ -37633,27 +37592,6 @@ "version": "0.2.5", "license": "MIT" }, - "node_modules/react-dnd": { - "version": "11.1.3", - "license": "MIT", - "dependencies": { - "@react-dnd/shallowequal": "^2.0.0", - "@types/hoist-non-react-statics": "^3.3.1", - "dnd-core": "^11.1.3", - "hoist-non-react-statics": "^3.3.0" - }, - "peerDependencies": { - "react": ">= 16.9.0", - "react-dom": ">= 16.9.0" - } - }, - "node_modules/react-dnd-html5-backend": { - "version": "11.1.3", - "license": "MIT", - "dependencies": { - "dnd-core": "^11.1.3" - } - }, "node_modules/react-docgen": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-6.0.4.tgz", @@ -37988,10 +37926,6 @@ "url": "https://opencollective.com/jss" } }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "license": "MIT" - }, "node_modules/react-moment": { "version": "1.1.1", "license": "MIT", @@ -38179,24 +38113,6 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/react-sortable-tree": { - "version": "2.8.0", - "license": "MIT", - "dependencies": { - "frontend-collective-react-dnd-scrollzone": "^1.0.2", - "lodash.isequal": "^4.5.0", - "prop-types": "^15.6.1", - "react-dnd": "^11.1.3", - "react-dnd-html5-backend": "^11.1.3", - "react-lifecycles-compat": "^3.0.4", - "react-virtualized": "^9.21.2" - }, - "peerDependencies": { - "react": "^16.3.0", - "react-dnd": "^7.3.0", - "react-dom": "^16.3.0" - } - }, "node_modules/react-style-singleton": { "version": "2.2.1", "license": "MIT", @@ -38232,22 +38148,6 @@ "react-dom": ">=16.6.0" } }, - "node_modules/react-virtualized": { - "version": "9.22.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.7.2", - "clsx": "^1.0.4", - "dom-helpers": "^5.1.3", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0-alpha", - "react-dom": "^15.3.0 || ^16.0.0-alpha" - } - }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -38429,14 +38329,6 @@ "node": ">=8" } }, - "node_modules/redux": { - "version": "4.0.5", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -39399,14 +39291,6 @@ "node": ">=10" } }, - "node_modules/scheduler": { - "version": "0.19.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "node_modules/scuid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz", @@ -45868,6 +45752,41 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "optional": true }, + "@dnd-kit/accessibility": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz", + "integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@dnd-kit/core": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz", + "integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==", + "requires": { + "@dnd-kit/accessibility": "^3.0.0", + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0" + } + }, + "@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "requires": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + } + }, + "@dnd-kit/utilities": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz", + "integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==", + "requires": { + "tslib": "^2.0.0" + } + }, "@editorjs/editorjs": { "version": "2.24.3", "requires": { @@ -49788,15 +49707,6 @@ "tslib": "^2.3.0" } }, - "@react-dnd/asap": { - "version": "4.0.0" - }, - "@react-dnd/invariant": { - "version": "2.0.0" - }, - "@react-dnd/shallowequal": { - "version": "2.0.0" - }, "@react-editor-js/client": { "version": "2.0.6", "requires": { @@ -57216,29 +57126,12 @@ "react-sortable-hoc": "*" } }, - "@types/react-sortable-tree": { - "version": "0.3.15", - "dev": true, - "requires": { - "@types/react": "*", - "@types/react-virtualized": "*", - "react-dnd": "^11.1.3" - } - }, "@types/react-transition-group": { "version": "4.4.1", "requires": { "@types/react": "*" } }, - "@types/react-virtualized": { - "version": "9.21.21", - "dev": true, - "requires": { - "@types/prop-types": "*", - "@types/react": "^17" - } - }, "@types/resolve": { "version": "1.17.1", "dev": true, @@ -60736,14 +60629,6 @@ "path-type": "^4.0.0" } }, - "dnd-core": { - "version": "11.1.3", - "requires": { - "@react-dnd/asap": "^4.0.0", - "@react-dnd/invariant": "^2.0.0", - "redux": "^4.0.4" - } - }, "doctrine": { "version": "3.0.0", "devOptional": true, @@ -63225,37 +63110,6 @@ "js-yaml": "^3.13.1" } }, - "frontend-collective-react-dnd-scrollzone": { - "version": "1.0.2", - "requires": { - "hoist-non-react-statics": "^3.1.0", - "lodash.throttle": "^4.0.1", - "prop-types": "^15.5.9", - "raf": "^3.2.0", - "react": "^16.3.0", - "react-display-name": "^0.2.0", - "react-dom": "^16.3.0" - }, - "dependencies": { - "react": { - "version": "16.14.0", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - } - }, - "react-dom": { - "version": "16.14.0", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - } - } - } - }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -66532,7 +66386,8 @@ "devOptional": true }, "lodash.isequal": { - "version": "4.5.0" + "version": "4.5.0", + "optional": true }, "lodash.isfunction": { "version": "3.0.9", @@ -66598,9 +66453,6 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, - "lodash.throttle": { - "version": "4.1.1" - }, "lodash.topairs": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", @@ -68723,7 +68575,8 @@ "optional": true }, "performance-now": { - "version": "2.1.0" + "version": "2.1.0", + "optional": true }, "picocolors": { "version": "1.0.0", @@ -69287,12 +69140,6 @@ "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", "dev": true }, - "raf": { - "version": "3.4.1", - "requires": { - "performance-now": "^2.1.0" - } - }, "ramda": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", @@ -69344,21 +69191,6 @@ "react-display-name": { "version": "0.2.5" }, - "react-dnd": { - "version": "11.1.3", - "requires": { - "@react-dnd/shallowequal": "^2.0.0", - "@types/hoist-non-react-statics": "^3.3.1", - "dnd-core": "^11.1.3", - "hoist-non-react-statics": "^3.3.0" - } - }, - "react-dnd-html5-backend": { - "version": "11.1.3", - "requires": { - "dnd-core": "^11.1.3" - } - }, "react-docgen": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-6.0.4.tgz", @@ -69592,9 +69424,6 @@ } } }, - "react-lifecycles-compat": { - "version": "3.0.4" - }, "react-moment": { "version": "1.1.1" }, @@ -69727,18 +69556,6 @@ "prop-types": "^15.5.7" } }, - "react-sortable-tree": { - "version": "2.8.0", - "requires": { - "frontend-collective-react-dnd-scrollzone": "^1.0.2", - "lodash.isequal": "^4.5.0", - "prop-types": "^15.6.1", - "react-dnd": "^11.1.3", - "react-dnd-html5-backend": "^11.1.3", - "react-lifecycles-compat": "^3.0.4", - "react-virtualized": "^9.21.2" - } - }, "react-style-singleton": { "version": "2.2.1", "requires": { @@ -69756,17 +69573,6 @@ "prop-types": "^15.6.2" } }, - "react-virtualized": { - "version": "9.22.3", - "requires": { - "@babel/runtime": "^7.7.2", - "clsx": "^1.0.4", - "dom-helpers": "^5.1.3", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-lifecycles-compat": "^3.0.4" - } - }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -69903,13 +69709,6 @@ "strip-indent": "^3.0.0" } }, - "redux": { - "version": "4.0.5", - "requires": { - "loose-envify": "^1.4.0", - "symbol-observable": "^1.2.0" - } - }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -70561,13 +70360,6 @@ "xmlchars": "^2.2.0" } }, - "scheduler": { - "version": "0.19.1", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "scuid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz", diff --git a/package.json b/package.json index f2ffc211886..944f406b090 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "saleor-dashboard", - "version": "3.18.0-dev", + "version": "3.19.0-dev", "main": "src/index.tsx", "repository": { "type": "git", @@ -18,6 +18,9 @@ }, "dependencies": { "@apollo/client": "3.4.17", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@editorjs/editorjs": "^2.24.3", "@editorjs/header": "^2.6.2", "@editorjs/image": "^2.6.2", @@ -91,7 +94,6 @@ "react-router": "^5.0.1", "react-router-dom": "^5.0.1", "react-sortable-hoc": "^1.10.1", - "react-sortable-tree": "^2.6.2", "remark-gfm": "^3.0.1", "slugify": "^1.4.6", "tslib": "^2.4.1", @@ -146,7 +148,6 @@ "@types/react-infinite-scroller": "^1.2.3", "@types/react-router-dom": "^4.3.4", "@types/react-sortable-hoc": "^0.7.1", - "@types/react-sortable-tree": "^0.3.15", "@types/url-join": "^4.0.1", "@types/uuid": "^9.0.4", "@types/webappsec-credential-management": "^0.5.1", @@ -342,7 +343,6 @@ "lint": "eslint \"src/**/*.@(tsx|ts|jsx|js)\" --fix", "lint:check-progress": "eslint-nibble \"src/**/*.@(tsx|ts|jsx|js)\"", "postbuild": "./scripts/sentry.sh", - "postinstall": "node scripts/patchReactVirtualized.js", "predev": "npm run build-types", "release": "echo $npm_package_version | xargs git tag && git push --follow-tags", "prepare": "is-ci || husky install", diff --git a/playwright.config.ts b/playwright.config.ts index f0fbc93108a..d4540f0819f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,8 +9,8 @@ export default defineConfig({ fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: "html", + workers: process.env.CI ? 2 : undefined, + reporter: process.env.CI ? "blob" : "html", timeout: 60000, // webServer: { // command: "npm run dev", diff --git a/playwright/data/url.ts b/playwright/data/url.ts index 8eeeff7f776..364a86a54c7 100644 --- a/playwright/data/url.ts +++ b/playwright/data/url.ts @@ -13,21 +13,21 @@ export const URL_LIST = { draftOrders: "orders/drafts/", giftCards: "gift-cards/", homePage: "/", - newPassword: "new-password/", - navigation: "navigation/", - orders: "orders/", - pages: "pages/", - pageTypes: "page-types/", - permissionsGroups: "permission-groups/", - plugins: "plugins/", - products: "products/", + newPassword: "/new-password/", + navigation: "/navigation/", + orders: "/orders/", + pages: "/pages/", + pageTypes: "/page-types/", + permissionsGroups: "/permission-groups/", + plugins: "/plugins/", + products: "/products/", productsAdd: "add?product-type-id=", - productTypes: "product-types/", - productTypesAdd: "product-types/add", - sales: "discounts/sales/", - shippingMethods: "shipping/", - siteSettings: "site-settings/", - staffMembers: "staff/", + productTypes: "/product-types/", + productTypesAdd: "/product-types/add", + sales: "/discounts/sales/", + shippingMethods: "/shipping/", + siteSettings: "/site-settings/", + staffMembers: "/staff/", stripeApiPaymentMethods: "https://api.stripe.com/v1/payment_methods", translations: "translations/", variants: "variant/", diff --git a/playwright/pages/loginPage.ts b/playwright/pages/loginPage.ts index 8392bc8d3a5..41a64a7ac0e 100644 --- a/playwright/pages/loginPage.ts +++ b/playwright/pages/loginPage.ts @@ -37,7 +37,7 @@ export class LoginPage { page: Page, path: string, ) { - await this.goto(); + await page.goto(process.env.BASE_URL!); await this.typeEmail(userEmail); await this.typePassword(userPassword); await this.clickSignInButton(); @@ -46,7 +46,7 @@ export class LoginPage { await page.context().storageState({ path }); } async basicUiLogin(userEmail: string, userPassword: string) { - await this.goto(); + await this.page.goto(process.env.BASE_URL!); await this.typeEmail(userEmail); await this.typePassword(userPassword); await this.clickSignInButton(); @@ -61,12 +61,4 @@ export class LoginPage { async clickSignInButton() { await this.signInButton.click(); } - async goto() { - const BASE_URL = process.env.BASE_URL; - const loginPageUrl = - BASE_URL === "http://localhost:9000/" - ? "http://localhost:9000/" - : "/dashboard"; - await this.page.goto(loginPageUrl); - } } diff --git a/playwright/tests/auth.setup.ts b/playwright/tests/auth.setup.ts index 141bf12b3a1..a9947b65751 100644 --- a/playwright/tests/auth.setup.ts +++ b/playwright/tests/auth.setup.ts @@ -24,15 +24,15 @@ const unauthenticatedUserPermissionsFile = setup("authenticate as admin", async ({ page }) => { const loginPage = await new LoginPage(page); await loginPage.loginAndSetStorageState( - process.env.CYPRESS_USER_NAME!, - process.env.CYPRESS_USER_PASSWORD!, + process.env.E2E_USER_NAME!, + process.env.E2E_USER_PASSWORD!, page, adminFile, ); }); setup("unauthenticated user ", async ({ page }) => { const loginPage = await new LoginPage(page); - await loginPage.goto(); + await page.goto("/"); await loginPage.resetPasswordLink.waitFor({ state: "visible" }); // End of authentication steps. await page @@ -43,7 +43,7 @@ setup("authenticate as user with discount permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.discount, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, discountPermissionsFile, ); @@ -53,7 +53,7 @@ setup("authenticate as user with orders permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.order, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, ordersPermissionsFile, ); @@ -62,7 +62,7 @@ setup("authenticate as user with apps permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.app, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, appsPermissionsFile, ); @@ -71,7 +71,7 @@ setup("authenticate as user with channels permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.channel, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, channelsWebhooksPermissionsFile, ); @@ -80,7 +80,7 @@ setup("authenticate as user with customer permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.customer, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, customerWebhooksPermissionsFile, ); @@ -89,7 +89,7 @@ setup("authenticate as user with gift cards permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.giftCard, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, giftCardsPermissionsFile, ); @@ -100,7 +100,7 @@ setup( const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.page, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, contentPermissionsFile, ); @@ -110,7 +110,7 @@ setup("authenticate as user with plugins permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.plugin, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, pluginPermissionsFile, ); @@ -121,7 +121,7 @@ setup( const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.productTypeAndAttribute, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, productTypePermissionsFile, ); @@ -131,7 +131,7 @@ setup("authenticate as user with settings permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.settings, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, settingsPermissionsFile, ); @@ -142,7 +142,7 @@ setup( const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.staff, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, staffMemberPermissionsFile, ); @@ -152,7 +152,7 @@ setup("authenticate as user with shipping permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.shipping, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, shippingPermissionsFile, ); @@ -161,7 +161,7 @@ setup("authenticate as user with translation permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.translations, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, translationPermissionsFile, ); @@ -170,7 +170,7 @@ setup("authenticate as user with product permissions", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.loginAndSetStorageState( USER_PERMISSION.product, - process.env.CYPRESS_PERMISSIONS_USERS_PASSWORD!, + process.env.E2E_PERMISSIONS_USERS_PASSWORD!, page, productPermissionsFile, ); diff --git a/schema.graphql b/schema.graphql index 83f144a7402..1cbdbf620c9 100644 --- a/schema.graphql +++ b/schema.graphql @@ -4482,7 +4482,7 @@ type Checkout implements Node & ObjectWithMetadata { discountName: String """ - Determines whether checkout prices should include taxes when displayed in a storefront. + Determines whether displayed prices should include taxes. Added in Saleor 3.9. """ @@ -4674,6 +4674,15 @@ type Checkout implements Node & ObjectWithMetadata { """ user: User + """ + The voucher assigned to the checkout. + + Added in Saleor 3.18. + + Requires one of the following permissions: MANAGE_DISCOUNTS. + """ + voucher: Voucher + """The code of voucher assigned to the checkout.""" voucherCode: String } @@ -7314,6 +7323,13 @@ type DiscountError { """List of products IDs which causes the error.""" products: [ID!] + + """ + List of voucher codes which causes the error. + + Added in Saleor 3.18. + """ + voucherCodes: [String!] } """An enumeration.""" @@ -7326,6 +7342,7 @@ enum DiscountErrorCode { NOT_FOUND REQUIRED UNIQUE + VOUCHER_ALREADY_USED } enum DiscountStatusEnum { @@ -7351,7 +7368,7 @@ enum DistanceUnitsEnum { YD } -"""Represents shop's domain.""" +"""Represents API domain.""" type Domain { """The host name of the domain.""" host: String! @@ -7359,7 +7376,7 @@ type Domain { """Inform if SSL is enabled.""" sslEnabled: Boolean! - """Shop's absolute URL.""" + """The absolute URL of the API.""" url: String! } @@ -7441,6 +7458,13 @@ input DraftOrderCreateInput { """ID of the voucher associated with the order.""" voucher: ID + + """ + A code of the voucher associated with the order. + + Added in Saleor 3.18. + """ + voucherCode: String } """ @@ -7537,6 +7561,13 @@ input DraftOrderInput { """ID of the voucher associated with the order.""" voucher: ID + + """ + A code of the voucher associated with the order. + + Added in Saleor 3.18. + """ + voucherCode: String } """ @@ -7987,6 +8018,40 @@ enum ExportScope { IDS } +""" +Export voucher codes to csv/xlsx file. + +Added in Saleor 3.18. + +Note: this API is currently in Feature Preview and can be subject to changes at later point. + +Requires one of the following permissions: MANAGE_DISCOUNTS. + +Triggers the following webhook events: +- VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file. +""" +type ExportVoucherCodes { + errors: [ExportError!]! + + """ + The newly created export file job which is responsible for export data. + """ + exportFile: ExportFile +} + +input ExportVoucherCodesInput { + """Type of exported file.""" + fileType: FileTypesEnum! + + """List of voucher code IDs to export.""" + ids: [ID!] + + """ + The ID of the voucher. If provided, exports all codes belonging to the voucher. + """ + voucherId: ID +} + """External authentication plugin.""" type ExternalAuthentication { """ID of external authentication plugin.""" @@ -8183,10 +8248,24 @@ type Fulfillment implements Node & ObjectWithMetadata { Added in Saleor 3.3. """ privateMetafields(keys: [String!]): Metadata + + """ + Amount of refunded shipping price. + + Added in Saleor 3.14. + """ + shippingRefundedAmount: Money status: FulfillmentStatus! """User-friendly fulfillment status.""" statusDisplay: String + + """ + Total refunded amount assigned to this fulfillment. + + Added in Saleor 3.14. + """ + totalRefundedAmount: Money trackingNumber: String! """Warehouse from fulfillment was fulfilled.""" @@ -13328,6 +13407,23 @@ type Mutation { input: ExportProductsInput! ): ExportProducts + """ + Export voucher codes to csv/xlsx file. + + Added in Saleor 3.18. + + Note: this API is currently in Feature Preview and can be subject to changes at later point. + + Requires one of the following permissions: MANAGE_DISCOUNTS. + + Triggers the following webhook events: + - VOUCHER_CODE_EXPORT_COMPLETED (async): A notification for the exported file. + """ + exportVoucherCodes( + """Fields required to export voucher codes.""" + input: ExportVoucherCodesInput! + ): ExportVoucherCodes + """Prepare external authentication URL for user by custom plugin.""" externalAuthenticationUrl( """The data required by plugin to create external authentication url.""" @@ -15898,6 +15994,9 @@ type Mutation { Note: this API is currently in Feature Preview and can be subject to changes at later point. Requires one of the following permissions: MANAGE_PRODUCTS. + + Triggers the following webhook events: + - PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated. """ stockBulkUpdate( """Policies of error handling. DEFAULT: REJECT_EVERYTHING""" @@ -16135,7 +16234,7 @@ type Mutation { """ transactionInitialize( """ - The expected action called for the transaction. By default, the `channel.defaultTransactionFlowStrategy` will be used. The field can be used only by app that has `HANDLE_PAYMENTS` permission. + The expected action called for the transaction. By default, the `channel.paymentSettings.defaultTransactionFlowStrategy` will be used.The field can be used only by app that has `HANDLE_PAYMENTS` permission. """ action: TransactionFlowStrategyEnum @@ -16411,6 +16510,21 @@ type Mutation { input: VoucherChannelListingInput! ): VoucherChannelListingUpdate + """ + Deletes voucher codes. + + Added in Saleor 3.18. + + Requires one of the following permissions: MANAGE_DISCOUNTS. + + Triggers the following webhook events: + - VOUCHER_UPDATED (async): A voucher was updated. + """ + voucherCodeBulkDelete( + """List of voucher codes IDs to delete.""" + ids: [ID!]! + ): VoucherCodeBulkDelete + """ Creates a new voucher. @@ -16656,7 +16770,7 @@ type Order implements Node & ObjectWithMetadata { discounts: [OrderDiscount!]! """ - Determines whether checkout prices should include taxes when displayed in a storefront. + Determines whether displayed prices should include taxes. Added in Saleor 3.9. """ @@ -16965,6 +17079,13 @@ type Order implements Node & ObjectWithMetadata { """ userEmail: String voucher: Voucher + + """ + Voucher code that was used for Order. + + Added in Saleor 3.18. + """ + voucherCode: String weight: Weight! } @@ -17179,9 +17300,7 @@ input OrderBulkCreateInput { """List of discounts.""" discounts: [OrderDiscountCommonInput!] - """ - Determines whether checkout prices should include taxes, when displayed in a storefront. - """ + """Determines whether displayed prices should include taxes.""" displayGrossPrices: Boolean """External ID of the order.""" @@ -17228,9 +17347,20 @@ input OrderBulkCreateInput { """Customer associated with the order.""" user: OrderBulkCreateUserInput! - """Code of a voucher associated with the order.""" + """ + Code of a voucher associated with the order. + + DEPRECATED: this field will be removed in Saleor 3.19. Use `voucherCode` instead. + """ voucher: String + """ + Code of a voucher associated with the order. + + Added in Saleor 3.18. + """ + voucherCode: String + """Weight of the order in kg.""" weight: WeightScalar } @@ -17738,6 +17868,8 @@ enum OrderErrorCode { INSUFFICIENT_STOCK INVALID INVALID_QUANTITY + INVALID_VOUCHER + INVALID_VOUCHER_CODE NOT_AVAILABLE_IN_CHANNEL NOT_EDITABLE NOT_FOUND @@ -18538,6 +18670,13 @@ type OrderLine implements Node & ObjectWithMetadata { """ quantityToFulfill: Int! + """ + Denormalized sale ID, set when order line is created for a product variant that is on sale. + + Added in Saleor 3.14. + """ + saleId: ID + """ Denormalized tax class of the product in this order line. @@ -18617,6 +18756,13 @@ type OrderLine implements Node & ObjectWithMetadata { """ variant: ProductVariant variantName: String! + + """ + Voucher code that was used for this order line. + + Added in Saleor 3.14. + """ + voucherCode: String } input OrderLineCreateInput { @@ -19008,15 +19154,6 @@ type OrderSettings { """ automaticallyFulfillNonShippableGiftCard: Boolean! - """ - Determine the transaction flow strategy to be used. Include the selected option in the payload sent to the payment app, as a requested action for the transaction. - - Added in Saleor 3.13. - - Note: this API is currently in Feature Preview and can be subject to changes at later point.This preview feature field will be removed in Saleor 3.17. Use `PaymentSettings.defaultTransactionFlowStrategy` instead. - """ - defaultTransactionFlowStrategy: TransactionFlowStrategyEnum! - """ The time in days after expired orders will be deleted. @@ -19035,6 +19172,15 @@ type OrderSettings { """ expireOrdersAfter: Minute + """ + Determine if voucher applied on draft order should be count toward voucher usage. + + Added in Saleor 3.18. + + Note: this API is currently in Feature Preview and can be subject to changes at later point. + """ + includeDraftOrderInVoucherUsage: Boolean! + """ Determine what strategy will be used to mark the order as paid. Based on the chosen option, the proper object will be created and attached to the order when it's manually marked as paid. `PAYMENT_FLOW` - [default option] creates the `Payment` object. @@ -19085,17 +19231,6 @@ input OrderSettingsInput { """ automaticallyFulfillNonShippableGiftCard: Boolean - """ - Determine the transaction flow strategy to be used. Include the selected option in the payload sent to the payment app, as a requested action for the transaction. - - Added in Saleor 3.13. - - Note: this API is currently in Feature Preview and can be subject to changes at later point. - - DEPRECATED: this preview feature field will be removed in Saleor 3.17. Use `PaymentSettingsInput.defaultTransactionFlowStrategy` instead. - """ - defaultTransactionFlowStrategy: TransactionFlowStrategyEnum - """ The time in days after expired orders will be deleted.Allowed range is from 1 to 120. @@ -19114,6 +19249,17 @@ input OrderSettingsInput { """ expireOrdersAfter: Minute + """ + Specify whether a coupon applied to draft orders will count toward voucher usage. + + Warning: when switching this setting from `false` to `true`, the vouchers will be disconnected from all draft orders. + + Added in Saleor 3.18. + + Note: this API is currently in Feature Preview and can be subject to changes at later point. + """ + includeDraftOrderInVoucherUsage: Boolean + """ Determine what strategy will be used to mark the order as paid. Based on the chosen option, the proper object will be created and attached to the order when it's manually marked as paid. `PAYMENT_FLOW` - [default option] creates the `Payment` object. @@ -19146,7 +19292,7 @@ input OrderSettingsUpdateInput { automaticallyConfirmAllNewOrders: Boolean """ - When enabled, all non-shippable gift card orders will be fulfilled automatically. By defualt set to True. + When enabled, all non-shippable gift card orders will be fulfilled automatically. By default set to True. """ automaticallyFulfillNonShippableGiftCard: Boolean } @@ -20228,6 +20374,13 @@ type Payment implements Node & ObjectWithMetadata { """Order associated with a payment.""" order: Order + """ + Informs whether this is a partial payment. + + Added in Saleor 3.14. + """ + partial: Boolean! + """Type of method used for payment.""" paymentMethodType: String! @@ -20250,6 +20403,13 @@ type Payment implements Node & ObjectWithMetadata { """ privateMetafields(keys: [String!]): Metadata + """ + PSP reference of the payment. + + Added in Saleor 3.14. + """ + pspReference: String + """Unique token associated with a payment.""" token: String! @@ -23006,7 +23166,7 @@ type ProductPricingInfo { discountLocalCurrency: TaxedMoney @deprecated(reason: "This field will be removed in Saleor 4.0. Always returns `null`.") """ - Determines whether this product's price displayed in a storefront should include taxes. + Determines whether displayed prices should include taxes. Added in Saleor 3.9. """ @@ -30045,6 +30205,9 @@ Added in Saleor 3.13. Note: this API is currently in Feature Preview and can be subject to changes at later point. Requires one of the following permissions: MANAGE_PRODUCTS. + +Triggers the following webhook events: +- PRODUCT_VARIANT_STOCK_UPDATED (async): A product variant stock details were updated. """ type StockBulkUpdate { """Returns how many objects were updated.""" @@ -30602,9 +30765,7 @@ type TaxConfiguration implements Node & ObjectWithMetadata { """List of country-specific exceptions in tax configuration.""" countries: [TaxConfigurationPerCountry!]! - """ - Determines whether prices displayed in a storefront should include taxes. - """ + """Determines whether displayed prices should include taxes.""" displayGrossPrices: Boolean! """The ID of the object.""" @@ -30693,7 +30854,7 @@ type TaxConfigurationPerCountry { country: CountryDisplay! """ - Determines whether prices displayed in a storefront should include taxes for this country. + Determines whether displayed prices should include taxes for this country. """ displayGrossPrices: Boolean! @@ -30711,7 +30872,7 @@ input TaxConfigurationPerCountryInput { countryCode: CountryCode! """ - Determines whether prices displayed in a storefront should include taxes for this country. + Determines whether displayed prices should include taxes for this country. """ displayGrossPrices: Boolean! @@ -30761,9 +30922,7 @@ input TaxConfigurationUpdateInput { """Determines whether taxes are charged in the given channel.""" chargeTaxes: Boolean - """ - Determines whether prices displayed in a storefront should include taxes. - """ + """Determines whether displayed prices should include taxes.""" displayGrossPrices: Boolean """Determines whether prices are entered with the tax included.""" @@ -32771,8 +32930,31 @@ type Voucher implements Node & ObjectWithMetadata { """ channelListings: [VoucherChannelListing!] - """The code of the voucher.""" - code: String! + """The code of the voucher.This field will be removed in Saleor 4.0.""" + code: String + + """ + List of codes available for this voucher. + + Added in Saleor 3.18. + """ + codes( + """Return the elements in the list that come after the specified cursor.""" + after: String + + """Return the elements in the list that come before the specified cursor.""" + before: String + + """ + Retrieve the first n elements from the list. Note that the system only allows fetching a maximum of 100 objects in a single query. + """ + first: Int + + """ + Retrieve the last n elements from the list. Note that the system only allows fetching a maximum of 100 objects in a single query. + """ + last: Int + ): VoucherCodeCountableConnection """ List of collections this voucher applies to. @@ -32888,6 +33070,15 @@ type Voucher implements Node & ObjectWithMetadata { last: Int ): ProductCountableConnection + """ + Determine if the voucher codes can be used once or multiple times. + + Added in Saleor 3.18. + + Note: this API is currently in Feature Preview and can be subject to changes at later point. + """ + singleUse: Boolean! + """The start date and time of voucher.""" startDate: DateTime! @@ -33016,6 +33207,109 @@ type VoucherChannelListingUpdate { voucher: Voucher } +""" +Represents voucher code. + +Added in Saleor 3.18. + +Note: this API is currently in Feature Preview and can be subject to changes at later point. +""" +type VoucherCode { + """Code to use the voucher.""" + code: String + + """Date time of code creation.""" + createdAt: DateTime! + + """The ID of the voucher code.""" + id: ID! + + """Whether a code is active or not.""" + isActive: Boolean + + """Number of times a code has been used.""" + used: Int +} + +""" +Deletes voucher codes. + +Added in Saleor 3.18. + +Requires one of the following permissions: MANAGE_DISCOUNTS. + +Triggers the following webhook events: +- VOUCHER_UPDATED (async): A voucher was updated. +""" +type VoucherCodeBulkDelete { + """Returns how many codes were deleted.""" + count: Int! + errors: [VoucherCodeBulkDeleteError!]! +} + +type VoucherCodeBulkDeleteError { + """The error code.""" + code: VoucherCodeBulkDeleteErrorCode! + + """The error message.""" + message: String + + """ + Path to field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. + """ + path: String + + """List of voucher codes which causes the error.""" + voucherCodes: [ID!] +} + +"""An enumeration.""" +enum VoucherCodeBulkDeleteErrorCode { + GRAPHQL_ERROR + INVALID + NOT_FOUND +} + +type VoucherCodeCountableConnection { + edges: [VoucherCodeCountableEdge!]! + + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """A total count of items in the collection.""" + totalCount: Int +} + +type VoucherCodeCountableEdge { + """A cursor for use in pagination.""" + cursor: String! + + """The item at the end of the edge.""" + node: VoucherCode! +} + +""" +Event sent when voucher code export is completed. + +Added in Saleor 3.18. +""" +type VoucherCodeExportCompleted implements Event { + """The export file for voucher codes.""" + export: ExportFile + + """Time of the event.""" + issuedAt: DateTime + + """The user or application that triggered the event.""" + issuingPrincipal: IssuingPrincipal + + """The application receiving the webhook.""" + recipient: App + + """Saleor version that triggered the event.""" + version: String +} + type VoucherCountableConnection { edges: [VoucherCountableEdge!]! @@ -33129,6 +33423,15 @@ input VoucherFilterInput { } input VoucherInput { + """ + List of codes to add. + + Added in Saleor 3.18. + + Note: this API is currently in Feature Preview and can be subject to changes at later point. + """ + addCodes: [String!] + """Voucher should be applied once per customer.""" applyOncePerCustomer: Boolean @@ -33138,7 +33441,9 @@ input VoucherInput { """Categories discounted by the voucher.""" categories: [ID!] - """Code to use the voucher.""" + """ + Code to use the voucher. This field will be removed in Saleor 4.0. Use `addCodes` instead. + """ code: String """Collections discounted by the voucher.""" @@ -33165,6 +33470,17 @@ input VoucherInput { """Products discounted by the voucher.""" products: [ID!] + """ + When set to 'True', each voucher code can be used only once; otherwise, codes can be used multiple times depending on `usageLimit`. + + The option can only be changed if none of the voucher codes have been used. + + Added in Saleor 3.18. + + Note: this API is currently in Feature Preview and can be subject to changes at later point. + """ + singleUse: Boolean + """Start date of the voucher in ISO 8601 format.""" startDate: DateTime @@ -33224,7 +33540,11 @@ type VoucherRemoveCatalogues { } enum VoucherSortField { - """Sort vouchers by code.""" + """ + Sort vouchers by code. + + DEPRECATED: this field will be removed in Saleor 4.0. + """ CODE """Sort vouchers by end date.""" @@ -33237,6 +33557,13 @@ enum VoucherSortField { """ MINIMUM_SPENT_AMOUNT + """ + Sort vouchers by name. + + Added in Saleor 3.18. + """ + NAME + """Sort vouchers by start date.""" START_DATE @@ -34357,7 +34684,9 @@ enum WebhookEventTypeAsyncEnum { """A new product variant is created.""" PRODUCT_VARIANT_CREATED - """A product variant is deleted.""" + """ + A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. + """ PRODUCT_VARIANT_DELETED """ @@ -34476,6 +34805,13 @@ enum WebhookEventTypeAsyncEnum { """A translation is updated.""" TRANSLATION_UPDATED + """ + A voucher code export is completed. + + Added in Saleor 3.18. + """ + VOUCHER_CODE_EXPORT_COMPLETED + """A new voucher created.""" VOUCHER_CREATED @@ -34939,7 +35275,9 @@ enum WebhookEventTypeEnum { """A new product variant is created.""" PRODUCT_VARIANT_CREATED - """A product variant is deleted.""" + """ + A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. + """ PRODUCT_VARIANT_DELETED """ @@ -35091,6 +35429,13 @@ enum WebhookEventTypeEnum { """A translation is updated.""" TRANSLATION_UPDATED + """ + A voucher code export is completed. + + Added in Saleor 3.18. + """ + VOUCHER_CODE_EXPORT_COMPLETED + """A new voucher created.""" VOUCHER_CREATED @@ -35338,6 +35683,7 @@ enum WebhookSampleEventTypeEnum { TRANSACTION_ITEM_METADATA_UPDATED TRANSLATION_CREATED TRANSLATION_UPDATED + VOUCHER_CODE_EXPORT_COMPLETED VOUCHER_CREATED VOUCHER_DELETED VOUCHER_METADATA_UPDATED diff --git a/scripts/patchReactVirtualized.js b/scripts/patchReactVirtualized.js deleted file mode 100644 index afbd9371340..00000000000 --- a/scripts/patchReactVirtualized.js +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const fs = require("fs"); - -/* - react-virtualized has broken ESM file, we need to patch this manually: - Ref: https://github.com/vitejs/vite/issues/1652#issuecomment-765875146 -*/ - -const dep = - "node_modules/react-virtualized/dist/es/WindowScroller/utils/onScroll.js"; -const code = fs - .readFileSync(dep, "utf-8") - .replace( - 'import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";', - "", - ); - -fs.writeFileSync(dep, code); diff --git a/src/attributes/components/AttributeDetails/messages.tsx b/src/attributes/components/AttributeDetails/messages.tsx index 603c167e0b6..9db9debca88 100644 --- a/src/attributes/components/AttributeDetails/messages.tsx +++ b/src/attributes/components/AttributeDetails/messages.tsx @@ -165,17 +165,22 @@ export const unitMessages = defineMessages({ }); export const units = { + cubicMillimeter: <>mm³, cubicCentimeter: <>cm³, cubicDecimeter: <>dm³, cubicMeter: <>m³, liter: "l", centimeter: "cm", + decimeter: "dm", meter: "m", + millimeter: "mm", kilometer: "km", gram: "g", kilogram: "kg", tonne: "t", + squareMillimeter: <>mm², squareCentimeter: <>cm², + squareDecimeter: <>dm², squareMeter: <>m², squareKilometer: <>km², cubicFoot: <>ft³, diff --git a/src/attributes/components/AttributeDetails/utils.ts b/src/attributes/components/AttributeDetails/utils.ts index 65d840968f6..4e8ef2fc20f 100644 --- a/src/attributes/components/AttributeDetails/utils.ts +++ b/src/attributes/components/AttributeDetails/utils.ts @@ -26,17 +26,22 @@ const UNIT_MESSAGES_MAPPING = { [MeasurementUnitsEnum.SQ_FT]: M.units.squareFt, [MeasurementUnitsEnum.SQ_YD]: M.units.squareYd, [MeasurementUnitsEnum.SQ_INCH]: M.units.squareInch, + [MeasurementUnitsEnum.CUBIC_MILLIMETER]: M.units.cubicMillimeter, [MeasurementUnitsEnum.CUBIC_CENTIMETER]: M.units.cubicCentimeter, [MeasurementUnitsEnum.CUBIC_DECIMETER]: M.units.cubicDecimeter, [MeasurementUnitsEnum.CUBIC_METER]: M.units.cubicMeter, [MeasurementUnitsEnum.LITER]: M.units.liter, [MeasurementUnitsEnum.CM]: M.units.centimeter, + [MeasurementUnitsEnum.DM]: M.units.decimeter, + [MeasurementUnitsEnum.MM]: M.units.millimeter, [MeasurementUnitsEnum.M]: M.units.meter, [MeasurementUnitsEnum.KM]: M.units.kilometer, [MeasurementUnitsEnum.G]: M.units.gram, [MeasurementUnitsEnum.KG]: M.units.kilogram, [MeasurementUnitsEnum.TONNE]: M.units.tonne, + [MeasurementUnitsEnum.SQ_MM]: M.units.squareMillimeter, [MeasurementUnitsEnum.SQ_CM]: M.units.squareCentimeter, + [MeasurementUnitsEnum.SQ_DM]: M.units.squareDecimeter, [MeasurementUnitsEnum.SQ_M]: M.units.squareMeter, [MeasurementUnitsEnum.SQ_KM]: M.units.squareKilometer, }; @@ -48,7 +53,7 @@ export const getMeasurementUnitMessage = ( const message = UNIT_MESSAGES_MAPPING[unit]; return typeof message === "string" || React.isValidElement(message) ? message - : formatMessage(message); + : formatMessage(message as MessageDescriptor); }; export const unitSystemChoices: Array> = [ @@ -107,13 +112,16 @@ export const unitMapping = { }, metric: { volume: [ + MeasurementUnitsEnum.CUBIC_MILLIMETER, MeasurementUnitsEnum.CUBIC_CENTIMETER, MeasurementUnitsEnum.CUBIC_DECIMETER, MeasurementUnitsEnum.CUBIC_METER, MeasurementUnitsEnum.LITER, ], distance: [ + MeasurementUnitsEnum.MM, MeasurementUnitsEnum.CM, + MeasurementUnitsEnum.DM, MeasurementUnitsEnum.M, MeasurementUnitsEnum.KM, ], @@ -123,7 +131,9 @@ export const unitMapping = { MeasurementUnitsEnum.TONNE, ], area: [ + MeasurementUnitsEnum.SQ_MM, MeasurementUnitsEnum.SQ_CM, + MeasurementUnitsEnum.SQ_DM, MeasurementUnitsEnum.SQ_M, MeasurementUnitsEnum.SQ_KM, ], diff --git a/src/auth/utils.ts b/src/auth/utils.ts index 4c1e14ef9a7..e9c0fb9425a 100644 --- a/src/auth/utils.ts +++ b/src/auth/utils.ts @@ -81,6 +81,7 @@ export const handleNestedMutationErrors = ({ intl, code: error.code, field: error.field, + voucherCodes: error.voucherCodes, }), }); }); diff --git a/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.tsx b/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.tsx index b33aa8db579..b5baf3baa15 100644 --- a/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.tsx +++ b/src/channels/components/ChannelDeleteDialog/ChannelDeleteDialog.tsx @@ -14,37 +14,43 @@ import { useStyles } from "../styles"; const messages = defineMessages({ deleteChannel: { - id: "QZoU0r", - defaultMessage: "Delete Channel", + id: "LATHyi", + defaultMessage: "Delete Channel: {channelSlug} ", description: "dialog header", }, deletingAllProductData: { - id: "Mz0cx+", + id: "GCho9N", defaultMessage: - "Deleting channel will delete all product data regarding this channel. Are you sure you want to delete this channel?", + "All channel settings information such as shipping, product listings, warehouse assignments, etc, will be lost.", description: "delete channel", }, needToBeMoved: { - id: "sidKce", - defaultMessage: - "All order information from this channel need to be moved to a different channel. Please select channel orders need to be moved to:.", + id: "BR8au7", + defaultMessage: "Select channel that you wish to move existing orders to.", description: "delete channel", }, + note: { + id: "wXFttp", + defaultMessage: "Note: Only channels with matching currency are available.", + description: "note on currency", + }, noAvailableChannel: { - id: "BXMSl4", + id: "Ge+dUe", defaultMessage: - "There is no available channel to move order information to. Please create a channel with same currency so that information can be moved to it.", + "To delete {channelSlug} you have to create a chanel with currency: {currency} to be able to move all existing orders.", description: "currency channel", }, selectChannel: { - id: "SZJhvK", - defaultMessage: "Select Channel", + id: "GP0zGO", + defaultMessage: "Select destination channel:", description: "dialog header", }, }); export interface ChannelDeleteDialogProps { channelsChoices: Choices; + channelSlug: string; + currency: string; hasOrders: boolean; confirmButtonState: ConfirmButtonTransitionState; open: boolean; @@ -55,10 +61,12 @@ export interface ChannelDeleteDialogProps { const ChannelDeleteDialog: React.FC = ({ channelsChoices = [], + channelSlug, hasOrders, confirmButtonState, open, onBack, + currency, onClose, onConfirm, }) => { @@ -75,21 +83,32 @@ const ChannelDeleteDialog: React.FC = ({ return ( (canBeDeleted ? onConfirm(choice) : onBack())} - title={intl.formatMessage(messages.deleteChannel)} + title={intl.formatMessage(messages.deleteChannel, { channelSlug })} confirmButtonLabel={intl.formatMessage( - canBeDeleted ? buttonMessages.delete : buttonMessages.ok, + canBeDeleted ? buttonMessages.delete : buttonMessages.cancel, )} - variant={canBeDeleted ? "delete" : "default"} + variant={canBeDeleted ? "delete" : "info"} >
{hasOrders ? ( hasChannels ? ( <> + + {intl.formatMessage(messages.deletingAllProductData)} + +
{intl.formatMessage(messages.needToBeMoved)} +
+ {intl.formatMessage(messages.note)}
= ({ onChange={e => setChoice(e.target.value)} />
- - {intl.formatMessage(messages.deletingAllProductData)} - ) : ( - {intl.formatMessage(messages.noAvailableChannel)} + {intl.formatMessage(messages.noAvailableChannel, { + channelSlug: {channelSlug}, + currency: {currency}, + })} ) ) : ( diff --git a/src/channels/views/ChannelDetails/ChannelDetails.tsx b/src/channels/views/ChannelDetails/ChannelDetails.tsx index 3eff4b8a405..f31775e4132 100644 --- a/src/channels/views/ChannelDetails/ChannelDetails.tsx +++ b/src/channels/views/ChannelDetails/ChannelDetails.tsx @@ -278,6 +278,8 @@ export const ChannelDetails: React.FC = ({ countries={shop?.countries || []} /> = ({ params }) => { {!!selectedChannel && ( ; }; @@ -42,15 +47,24 @@ const AssignAttributeValueDialog: React.FC = ({ entityType, pages, products, + attribute, ...rest }) => { const intl = useIntl(); + const filteredProducts = filterProductsByAttributeValues(products, attribute); + const filteredPages = filterPagesByAttributeValues(pages, attribute); + switch (entityType) { case AttributeEntityTypeEnum.PAGE: return ( ({ id: page.id, name: page.title }))} + containers={ + filteredPages?.map(page => ({ + id: page.id, + name: page.title, + })) ?? [] + } labels={{ confirmBtn: intl.formatMessage(pagesMessages.confirmBtn), label: intl.formatMessage(pagesMessages.searchLabel), @@ -61,9 +75,9 @@ const AssignAttributeValueDialog: React.FC = ({ /> ); case AttributeEntityTypeEnum.PRODUCT: - return ; + return ; case AttributeEntityTypeEnum.PRODUCT_VARIANT: - return ; + return ; } }; AssignAttributeValueDialog.displayName = "AssignAttributeValueDialog"; diff --git a/src/components/AssignAttributeValueDialog/utils.ts b/src/components/AssignAttributeValueDialog/utils.ts new file mode 100644 index 00000000000..9fa6428fb05 --- /dev/null +++ b/src/components/AssignAttributeValueDialog/utils.ts @@ -0,0 +1,38 @@ +import { SearchPagesQuery, SearchProductsQuery } from "@dashboard/graphql"; +import { RelayToFlat } from "@dashboard/types"; + +import { AttributeInput } from "../Attributes"; + +type ProductsToFilter = RelayToFlat; +type PagesToFilter = RelayToFlat; + +export const filterProductsByAttributeValues = ( + products: ProductsToFilter, + attribute: AttributeInput, +): ProductsToFilter => { + switch (attribute.data.entityType) { + case "PRODUCT": + return ( + products?.filter(product => !attribute.value.includes(product.id)) ?? [] + ); + case "PRODUCT_VARIANT": + return ( + products?.map(product => ({ + ...product, + variants: + product.variants?.filter( + variant => !attribute.value.includes(variant.id), + ) ?? [], + })) ?? [] + ); + default: + return products; + } +}; + +export const filterPagesByAttributeValues = ( + pages: PagesToFilter, + attribute: AttributeInput, +): PagesToFilter => { + return pages?.filter(page => !attribute.value.includes(page.id)) ?? []; +}; diff --git a/src/components/AssignContainerDialog/AssignContainerDialog.tsx b/src/components/AssignContainerDialog/AssignContainerDialog.tsx index e1065f222af..df6245acfed 100644 --- a/src/components/AssignContainerDialog/AssignContainerDialog.tsx +++ b/src/components/AssignContainerDialog/AssignContainerDialog.tsx @@ -77,16 +77,21 @@ const AssignContainerDialog: React.FC = props => { const classes = useStyles(props); const scrollableDialogClasses = useScrollableDialogStyle({}); - const [query, onQueryChange] = useSearchQuery(onFetch); + const [query, onQueryChange, queryReset] = useSearchQuery(onFetch); const [selectedContainers, setSelectedContainers] = React.useState< Container[] >([]); const handleSubmit = () => onSubmit(selectedContainers); + const handleClose = () => { + queryReset(); + onClose(); + }; + return ( = props => { })); }; + const handleClose = () => { + queryReset(); + onClose(); + }; + return ( = props => { const scrollableDialogClasses = useScrollableDialogStyle({}); const intl = useIntl(); - const [query, onQueryChange] = useSearchQuery(onFetch); + const [query, onQueryChange, queryReset] = useSearchQuery(onFetch); const [variants, setVariants] = React.useState([]); const productChoices = @@ -97,9 +97,14 @@ const AssignVariantDialog: React.FC = props => { })), ); + const handleClose = () => { + queryReset(); + onClose(); + }; + return ( void; + disabled?: boolean; children: React.ReactNode; } export const BulkDeleteButton = forwardRef< HTMLButtonElement, ProductListDeleteButtonProps ->(({ onClick, children }, ref) => { +>(({ onClick, children, disabled }, ref) => { const [isTooltipOpen, setIsTooltipOpen] = useState(false); return ( @@ -17,6 +18,7 @@ export const BulkDeleteButton = forwardRef< + + + + + + + + ); +}; diff --git a/src/discounts/components/DiscountRules/componenets/AddButton/index.ts b/src/discounts/components/DiscountRules/componenets/AddButton/index.ts new file mode 100644 index 00000000000..01087539fea --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/AddButton/index.ts @@ -0,0 +1 @@ +export * from "./AddButton"; diff --git a/src/discounts/components/DiscountRules/componenets/DiscountTypeSwitch/DiscountTypeSwitch.tsx b/src/discounts/components/DiscountRules/componenets/DiscountTypeSwitch/DiscountTypeSwitch.tsx new file mode 100644 index 00000000000..9dd34d0ad41 --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/DiscountTypeSwitch/DiscountTypeSwitch.tsx @@ -0,0 +1,51 @@ +import { Box, Switch, Text } from "@saleor/macaw-ui-next"; +import React from "react"; + +import { DiscountType } from "../../types"; + +interface DiscountTypeSwitchProps { + selected: DiscountType; + currencySymbol: string; + onChange: (type: string) => void; +} + +const PERCENT_SYMBOL = "%"; + +export const DiscountTypeSwitch = ({ + selected, + currencySymbol, + onChange, +}: DiscountTypeSwitchProps) => { + return ( + + + + {currencySymbol} + + + + + {PERCENT_SYMBOL} + + + + ); +}; diff --git a/src/discounts/components/DiscountRules/componenets/DiscountTypeSwitch/index.ts b/src/discounts/components/DiscountRules/componenets/DiscountTypeSwitch/index.ts new file mode 100644 index 00000000000..e075c66d333 --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/DiscountTypeSwitch/index.ts @@ -0,0 +1 @@ +export * from "./DiscountTypeSwitch"; diff --git a/src/discounts/components/DiscountRules/componenets/Placeholder/Placeholder.tsx b/src/discounts/components/DiscountRules/componenets/Placeholder/Placeholder.tsx new file mode 100644 index 00000000000..ecb45a46a54 --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/Placeholder/Placeholder.tsx @@ -0,0 +1,27 @@ +import { Box, PlusIcon, Text } from "@saleor/macaw-ui-next"; +import React from "react"; +import { useIntl } from "react-intl"; + +import { messages } from "../../messages"; + +export const Placeholder = () => { + const intl = useIntl(); + + return ( + + + {intl.formatMessage(messages.placeholder)} + + ); +}; diff --git a/src/discounts/components/DiscountRules/componenets/Placeholder/index.ts b/src/discounts/components/DiscountRules/componenets/Placeholder/index.ts new file mode 100644 index 00000000000..89dd624de7e --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/Placeholder/index.ts @@ -0,0 +1 @@ +export * from "./Placeholder"; diff --git a/src/discounts/components/DiscountRules/componenets/Rule/Rule.tsx b/src/discounts/components/DiscountRules/componenets/Rule/Rule.tsx new file mode 100644 index 00000000000..25eaafa1fe7 --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/Rule/Rule.tsx @@ -0,0 +1,49 @@ +import { Multiselect } from "@dashboard/components/Combobox"; +import { RichTextContext } from "@dashboard/utils/richText/context"; +import useRichText from "@dashboard/utils/richText/useRichText"; +import { Box, Input, Option } from "@saleor/macaw-ui-next"; +import React from "react"; + +import { DiscountRule } from "../../types"; +import { RuleAccordion } from "./components/RuleAccordion/RuleAccordion"; +import { RuleConditions } from "./components/RuleConditions"; +import { RuleDescription } from "./components/RuleDescription"; +import { RuleReward } from "./components/RuleReward"; + +interface RuleProps { + rule: DiscountRule; + channels: Option[]; +} + +export const Rule = ({ channels }: RuleProps) => { + const richText = useRichText({ + initial: "", + loading: false, + triggerChange: () => {}, + }); + + return ( + + + + + + {}} + fetchOptions={() => {}} + /> + + + + + + + + + + ); +}; diff --git a/src/discounts/components/DiscountRules/componenets/Rule/components/RuleAccordion/RuleAccordion.tsx b/src/discounts/components/DiscountRules/componenets/Rule/components/RuleAccordion/RuleAccordion.tsx new file mode 100644 index 00000000000..c4edd69c63c --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/Rule/components/RuleAccordion/RuleAccordion.tsx @@ -0,0 +1,30 @@ +import { Accordion, Text } from "@saleor/macaw-ui-next"; +import React, { ReactNode, useState } from "react"; + +interface RuleAccordionProps { + children: ReactNode; + title: ReactNode; +} + +export const RuleAccordion = ({ children, title }: RuleAccordionProps) => { + const [collapsedId, setCollapsedId] = useState(""); + + return ( + + + + {title} + + + {children} + + + ); +}; diff --git a/src/discounts/components/DiscountRules/componenets/Rule/components/RuleAccordion/index.ts b/src/discounts/components/DiscountRules/componenets/Rule/components/RuleAccordion/index.ts new file mode 100644 index 00000000000..59e720bc05b --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/Rule/components/RuleAccordion/index.ts @@ -0,0 +1 @@ +export * from "./RuleAccordion"; diff --git a/src/discounts/components/DiscountRules/componenets/Rule/components/RuleConditionRow/RuleConditionRow.tsx b/src/discounts/components/DiscountRules/componenets/Rule/components/RuleConditionRow/RuleConditionRow.tsx new file mode 100644 index 00000000000..a804093cfb9 --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/Rule/components/RuleConditionRow/RuleConditionRow.tsx @@ -0,0 +1,57 @@ +import { Combobox, Multiselect } from "@dashboard/components/Combobox"; +import { Box, Button, RemoveIcon, Select } from "@saleor/macaw-ui-next"; +import React from "react"; + +import { DiscountCondition } from "../../../../types"; +import { discountConditionTypes } from "./const"; + +interface DiscountConditionRowProps { + condition: DiscountCondition; + onRemove: () => void; +} + +export const RuleConditionRow = ({ + condition, + onRemove, +}: DiscountConditionRowProps) => { + return ( + + {}} + options={discountConditionTypes} + onChange={() => {}} + /> + + {}} + label={intl.formatMessage(messages.discountValue)} + /> + + + + ); +}; diff --git a/src/discounts/components/DiscountRules/componenets/Rule/components/RuleReward/index.ts b/src/discounts/components/DiscountRules/componenets/Rule/components/RuleReward/index.ts new file mode 100644 index 00000000000..950baa07d0e --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/Rule/components/RuleReward/index.ts @@ -0,0 +1 @@ +export * from "./RuleReward"; diff --git a/src/discounts/components/DiscountRules/componenets/Rule/index.ts b/src/discounts/components/DiscountRules/componenets/Rule/index.ts new file mode 100644 index 00000000000..79a4bfcb96a --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/Rule/index.ts @@ -0,0 +1 @@ +export * from "./Rule"; diff --git a/src/discounts/components/DiscountRules/componenets/RulesList/RulesList.tsx b/src/discounts/components/DiscountRules/componenets/RulesList/RulesList.tsx new file mode 100644 index 00000000000..dfa062f620d --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/RulesList/RulesList.tsx @@ -0,0 +1,30 @@ +import { Box } from "@saleor/macaw-ui-next"; +import React from "react"; + +import { DiscountRule } from "../../types"; +import { Placeholder } from "../Placeholder"; +import { Rule } from "../Rule"; + +interface RulesListProps { + rules: DiscountRule[]; +} + +export const RulesList = ({ rules }: RulesListProps) => { + if (rules.length === 0) { + return ; + } + return ( + + {rules.map(rule => ( + + ))} + + ); +}; diff --git a/src/discounts/components/DiscountRules/componenets/RulesList/index.ts b/src/discounts/components/DiscountRules/componenets/RulesList/index.ts new file mode 100644 index 00000000000..36ed60ab8ab --- /dev/null +++ b/src/discounts/components/DiscountRules/componenets/RulesList/index.ts @@ -0,0 +1 @@ +export * from "./RulesList"; diff --git a/src/discounts/components/DiscountRules/index.ts b/src/discounts/components/DiscountRules/index.ts new file mode 100644 index 00000000000..104270f77b0 --- /dev/null +++ b/src/discounts/components/DiscountRules/index.ts @@ -0,0 +1 @@ +export * from "./DiscountRules"; diff --git a/src/discounts/components/DiscountRules/messages.ts b/src/discounts/components/DiscountRules/messages.ts new file mode 100644 index 00000000000..a93c5cc99ff --- /dev/null +++ b/src/discounts/components/DiscountRules/messages.ts @@ -0,0 +1,36 @@ +import { defineMessages } from "react-intl"; + +export const messages = defineMessages({ + title: { + defaultMessage: "Rules", + id: "kAAlGL", + }, + addRule: { + defaultMessage: "Add rule", + id: "gzM1em", + }, + catalog: { + defaultMessage: "Catalog", + id: "GOdq5V", + }, + catalogDescription: { + defaultMessage: "Discount rules for products, collections or categories.", + id: "ucLtY8", + }, + placeholder: { + defaultMessage: "Add your first rule to set up a promotion", + id: "XtlUj6", + }, + conditions: { + defaultMessage: "Conditions", + id: "S8kqP9", + }, + discountValue: { + defaultMessage: "Discount value", + id: "kNK4es", + }, + reward: { + defaultMessage: "Reward", + id: "PuQb0P", + }, +}); diff --git a/src/discounts/components/DiscountRules/types.ts b/src/discounts/components/DiscountRules/types.ts new file mode 100644 index 00000000000..5d17c84dc60 --- /dev/null +++ b/src/discounts/components/DiscountRules/types.ts @@ -0,0 +1,14 @@ +import { Option } from "@saleor/macaw-ui-next"; + +export interface DiscountRule { + id: string; + name: string; + description: string; +} + +export interface DiscountCondition { + type: string; + values: Option[]; +} + +export type DiscountType = "fixed" | "percentage"; diff --git a/src/discounts/components/VoucherCodes/VoucherCodes.test.tsx b/src/discounts/components/VoucherCodes/VoucherCodes.test.tsx new file mode 100644 index 00000000000..2006755716e --- /dev/null +++ b/src/discounts/components/VoucherCodes/VoucherCodes.test.tsx @@ -0,0 +1,313 @@ +import { + mockResizeObserver, + prepareDatagridScroller, +} from "@dashboard/components/Datagrid/testUtils"; +import { ThemeProvider as LegacyThemeProvider } from "@saleor/macaw-ui"; +import { ThemeProvider } from "@saleor/macaw-ui-next"; +import { act, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React, { ReactNode } from "react"; +import { FormattedMessageProps } from "react-intl"; +import { BrowserRouter } from "react-router-dom"; + +import { VoucherCode } from "../VoucherCodesDatagrid/types"; +import { VoucherCodes, VoucherCodesProps } from "./VoucherCodes"; + +jest.mock("react-intl", () => ({ + useIntl: jest.fn(() => ({ + formatMessage: jest.fn(x => x.defaultMessage), + })), + defineMessages: jest.fn(x => x), + FormattedMessage: ({ defaultMessage }: FormattedMessageProps) => ( + <>{defaultMessage} + ), +})); + +const Wrapper = ({ children }: { children: ReactNode }) => { + return ( + + + {children} + + + ); +}; + +const renderVoucherCodes = (props: Partial) => { + const results = render( + , + { wrapper: Wrapper }, + ); + prepareDatagridScroller(); + + return results; +}; + +const codes: VoucherCode[] = [ + { code: "Code 1", isActive: true, used: 0 }, + { code: "Code 2", isActive: true, used: 0 }, + { code: "Code 3", isActive: true, used: 0 }, + { code: "Code 4", isActive: false, used: 0 }, +]; + +beforeAll(() => { + mockResizeObserver(); +}); + +describe("VoucherCodes", () => { + it("should render empty datagrid when no voucher codes", () => { + // Arrange & Act + renderVoucherCodes({}); + + // Assert + expect(screen.getByText(/^voucher codes$/i)).toBeInTheDocument(); + expect(screen.getByText(/^no voucher codes found$/i)).toBeInTheDocument(); + }); + + it("should render datagrid with voucher codes", async () => { + // Arrange & Act + renderVoucherCodes({ + codes, + }); + + // Assert + await waitFor(() => { + expect(screen.getByText(/^code 1$/i)).toBeInTheDocument(); + expect(screen.getByText(/^code 2$/i)).toBeInTheDocument(); + expect(screen.getByText(/^code 3$/i)).toBeInTheDocument(); + expect(screen.getByText(/^code 4$/i)).toBeInTheDocument(); + }); + }); + + it("should render spinner when loading", () => { + // Arrange & Act + renderVoucherCodes({ loading: true }); + + // Assert + expect(screen.getByRole("progressbar")).toBeInTheDocument(); + }); + + it("should not allow to delete selected codes when contains saved codes", async () => { + // Arrange & Act + const onDeleteCodes = jest.fn(); + + renderVoucherCodes({ + onDeleteCodes, + codes, + selectedCodesIds: ["Code 1", "Code 2"], + }); + + const deleteButton = screen.getByTestId("bulk-delete-button"); + + // Assert + expect(deleteButton).toBeDisabled(); + }); + + it("should allow to delete selected codes when selected only draft codes", async () => { + // Arrange + const onDeleteCodes = jest.fn(); + + renderVoucherCodes({ + onDeleteCodes, + codes: [ + ...codes, + { code: "Manual code 1", status: "Draft" }, + { code: "Manual code 2", status: "Draft" }, + { code: "Manual code 3", status: "Draft" }, + ], + selectedCodesIds: ["Manual code 1", "Manual code 1"], + }); + + const deleteButton = screen.getByTestId("bulk-delete-button"); + + // Act + await act(async () => { + await userEvent.click(deleteButton); + }); + + // Assert + expect(await screen.findByRole("dialog")).toBeInTheDocument(); + expect( + await screen.findByText( + /are you sure you want to delete these voucher codes?/i, + ), + ).toBeInTheDocument(); + + // Act + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: /delete/i })); + }); + + // Assert + expect(onDeleteCodes).toBeCalled(); + }); + + it("should allow to generate custom code", async () => { + // Arrange & Act + const onCustomCodeGenerate = jest.fn(); + + renderVoucherCodes({ + onCustomCodeGenerate, + }); + + const addCodeButton = screen.getByRole("button", { name: /add code/i }); + + // Act + await act(async () => { + await userEvent.click(addCodeButton); + }); + + await waitFor(() => { + expect(screen.getByText(/^manual$/i)).toBeInTheDocument(); + }); + + // Act + await act(async () => { + await userEvent.click(screen.getByText(/^manual$/i)); + }); + + // Assert + expect( + await screen.findByText(/^enter voucher code$/i), + ).toBeInTheDocument(); + + // Act + await userEvent.type(screen.getByRole("input"), "Test code"); + await userEvent.click(screen.getByRole("button", { name: /confirm/i })); + + // Assert + expect(onCustomCodeGenerate).toBeCalledWith("Test code"); + }); + + it("should allow to generate multiple code", async () => { + // Arrange & Act + const onMultiCodesGenerate = jest.fn(); + + renderVoucherCodes({ + onMultiCodesGenerate, + }); + + const addButton = screen.getByRole("button", { name: /add code/i }); + + // Act + await act(async () => { + await userEvent.click(addButton); + }); + + // Assert + expect( + await screen.findByText(/^auto-generate codes$/i), + ).toBeInTheDocument(); + + // Act + await act(async () => { + await userEvent.click(screen.getByText(/^auto-generate codes$/i)); + }); + + // Assert + expect( + await screen.findByText(/^generate Voucher Codes$/i), + ).toBeInTheDocument(); + + // Act + await userEvent.type( + screen.getByRole("input", { name: /^code quantity/i }), + "10", + ); + await userEvent.type( + screen.getByRole("input", { name: /^code prefix/i }), + "PREFIX", + ); + await userEvent.click(screen.getByRole("button", { name: /confirm/i })); + + // Assert + expect(onMultiCodesGenerate).toBeCalledWith({ + quantity: "10", + prefix: "PREFIX", + }); + }); + + it("should allow to load next page", async () => { + // Arrange & Act + const loadNextPage = jest.fn(); + + renderVoucherCodes({ + voucherCodesPagination: { + loadNextPage, + loadPreviousPage: jest.fn(), + paginatorType: "click", + pageInfo: { + endCursor: "", + hasNextPage: true, + hasPreviousPage: false, + startCursor: "", + }, + }, + }); + + // Assert + expect(screen.getByTestId("button-pagination-next")).toBeEnabled(); + expect(screen.getByTestId("button-pagination-back")).toBeDisabled(); + + // Act + await act(async () => { + await userEvent.click(screen.getByTestId("button-pagination-next")); + }); + + // Assert + expect(loadNextPage).toBeCalled(); + }); + + it("should allow to load previous page", async () => { + // Arrange & Act + const loadPreviousPage = jest.fn(); + + renderVoucherCodes({ + voucherCodesPagination: { + loadNextPage: jest.fn(), + loadPreviousPage, + paginatorType: "click", + pageInfo: { + endCursor: "", + hasNextPage: false, + hasPreviousPage: true, + startCursor: "", + }, + }, + }); + + // Assert + expect(screen.getByTestId("button-pagination-back")).toBeEnabled(); + expect(screen.getByTestId("button-pagination-next")).toBeDisabled(); + + // Act + await act(async () => { + await userEvent.click(screen.getByTestId("button-pagination-back")); + }); + + // Assert + expect(loadPreviousPage).toBeCalled(); + }); +}); diff --git a/src/discounts/components/VoucherCodes/VoucherCodes.tsx b/src/discounts/components/VoucherCodes/VoucherCodes.tsx index ec5f50f5de2..89c56e76fdb 100644 --- a/src/discounts/components/VoucherCodes/VoucherCodes.tsx +++ b/src/discounts/components/VoucherCodes/VoucherCodes.tsx @@ -18,8 +18,9 @@ import { } from "../VoucherCodesGenerateDialog"; import { VoucherCodesManualDialog } from "../VoucherCodesManualDialog"; import { VoucherCodesUrlDialog } from "./types"; +import { hasSavedVoucherCodesToDelete } from "./utils"; -interface VoucherCodesProps extends VoucherCodesDatagridProps { +export interface VoucherCodesProps extends VoucherCodesDatagridProps { selectedCodesIds: string[]; voucherCodesPagination: LocalPagination; settings: UseListSettings["settings"]; @@ -43,6 +44,11 @@ export const VoucherCodes = ({ null, ); + const hasSavedCodesToDelete = hasSavedVoucherCodesToDelete( + selectedCodesIds, + datagridProps.codes, + ); + const closeModal = () => { setOpenModal(null); }; @@ -63,8 +69,18 @@ export const VoucherCodes = ({ {selectedCodesIds.length > 0 && ( - setOpenModal("delete-codes")}> - + setOpenModal("delete-codes")} + > + {hasSavedCodesToDelete ? ( + + ) : ( + + )} )} { + it("should return true if there are saved voucher codes to delete", () => { + const voucherCodesIdsToDelete = ["voucherCode1", "voucherCode2"]; + const voucherCodes = [ + { + code: "voucherCode1", + isActive: true, + }, + { + code: "voucherCode1", + status: "Draft", + }, + { + code: "voucherCode2", + isActive: false, + }, + ]; + + expect( + hasSavedVoucherCodesToDelete(voucherCodesIdsToDelete, voucherCodes), + ).toBe(true); + }); + + it("should return false if there are no saved voucher codes to delete", () => { + const voucherCodesIdsToDelete = ["voucherCode1", "voucherCode2"]; + const voucherCodes = [ + { + code: "voucherCode1", + status: "Draft", + }, + { + code: "voucherCode2", + status: "Draft", + }, + ]; + + expect( + hasSavedVoucherCodesToDelete(voucherCodesIdsToDelete, voucherCodes), + ).toBe(false); + }); +}); diff --git a/src/discounts/components/VoucherCodes/utils.ts b/src/discounts/components/VoucherCodes/utils.ts new file mode 100644 index 00000000000..c60e59c02a9 --- /dev/null +++ b/src/discounts/components/VoucherCodes/utils.ts @@ -0,0 +1,14 @@ +import { VoucherCode } from "../VoucherCodesDatagrid/types"; + +export const hasSavedVoucherCodesToDelete = ( + voucherCodesIdsToDelete: string[], + voucherCodes: VoucherCode[], +): boolean => { + return voucherCodesIdsToDelete.some(voucherCodeId => { + const voucherCode = voucherCodes.find( + voucherCode => voucherCode.code === voucherCodeId, + ); + + return voucherCode && voucherCode.status !== "Draft"; + }); +}; diff --git a/src/discounts/components/VoucherCodesAddButton/VoucherCodesAddButton.tsx b/src/discounts/components/VoucherCodesAddButton/VoucherCodesAddButton.tsx index 37c9d5ccb62..282b5e8b446 100644 --- a/src/discounts/components/VoucherCodesAddButton/VoucherCodesAddButton.tsx +++ b/src/discounts/components/VoucherCodesAddButton/VoucherCodesAddButton.tsx @@ -55,6 +55,7 @@ export const VoucherCodesAddButton = ({ - diff --git a/src/discounts/components/VoucherCodesManualDialog/VoucherCodesManualDialog.tsx b/src/discounts/components/VoucherCodesManualDialog/VoucherCodesManualDialog.tsx index 00c02630968..d818cd2e38a 100644 --- a/src/discounts/components/VoucherCodesManualDialog/VoucherCodesManualDialog.tsx +++ b/src/discounts/components/VoucherCodesManualDialog/VoucherCodesManualDialog.tsx @@ -3,10 +3,9 @@ import { ConfirmButtonTransitionState, } from "@dashboard/components/ConfirmButton"; import { DashboardModal } from "@dashboard/components/Modal"; -import useForm from "@dashboard/hooks/useForm"; import { buttonMessages } from "@dashboard/intl"; import { Box, Button, Input } from "@saleor/macaw-ui-next"; -import React from "react"; +import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { messages } from "./messages"; @@ -18,14 +17,6 @@ interface VoucherCodesManualDialogProps { onSubmit: (code: string) => void; } -interface FormData { - code: string; -} - -const intialData: FormData = { - code: "", -}; - export const VoucherCodesManualDialog = ({ open, confirmButtonTransitionState, @@ -33,21 +24,27 @@ export const VoucherCodesManualDialog = ({ onSubmit, }: VoucherCodesManualDialogProps) => { const intl = useIntl(); - - const { data, change, submit, reset } = useForm( - intialData, - ({ code }: FormData) => onSubmit(code), - ); + const [error, setError] = useState(""); + const [code, setCode] = useState(""); const handleModalClose = () => { onClose(); - reset(); + setCode(""); + setError(""); }; const handleSubmit = async () => { - await submit(); - onClose(); - reset(); + try { + await onSubmit(code); + onClose(); + setCode(""); + } catch (e: unknown) { + if (e instanceof Error) { + if (e.message === "Code already exists") { + setError(intl.formatMessage(messages.codeExists)); + } + } + } }; return ( @@ -58,12 +55,18 @@ export const VoucherCodesManualDialog = ({ { + setCode(e.target.value); + setError(""); + }} /> @@ -71,6 +74,7 @@ export const VoucherCodesManualDialog = ({ {intl.formatMessage(buttonMessages.back)} diff --git a/src/discounts/components/VoucherCodesManualDialog/messages.ts b/src/discounts/components/VoucherCodesManualDialog/messages.ts index acaa779c8b6..7f4f23bca3c 100644 --- a/src/discounts/components/VoucherCodesManualDialog/messages.ts +++ b/src/discounts/components/VoucherCodesManualDialog/messages.ts @@ -9,4 +9,8 @@ export const messages = defineMessages({ defaultMessage: "Enter usage", id: "qPSWmL", }, + codeExists: { + id: "dHAwu8", + defaultMessage: "Code already exists", + }, }); diff --git a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx index 43fbeb52b71..92453ba5566 100644 --- a/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx +++ b/src/discounts/components/VoucherCreatePage/VoucherCreatePage.tsx @@ -11,7 +11,6 @@ import { } from "@dashboard/discounts/handlers"; import { voucherListUrl } from "@dashboard/discounts/urls"; import { VOUCHER_CREATE_FORM_ID } from "@dashboard/discounts/views/VoucherCreate/types"; -import { useFlag } from "@dashboard/featureFlags"; import { DiscountErrorFragment, PermissionEnum } from "@dashboard/graphql"; import useForm, { SubmitPromise } from "@dashboard/hooks/useForm"; import useNavigator from "@dashboard/hooks/useNavigator"; @@ -33,7 +32,7 @@ import VoucherValue from "../VoucherValue"; import { initialForm } from "./const"; import { useVoucherCodesPagination } from "./hooks/useVoucherCodesPagination"; import { useVoucherCodesSelection } from "./hooks/useVoucherCodesSelection"; -import { generateMultipleIds } from "./utils"; +import { generateMultipleIds, voucherCodeExists } from "./utils"; export interface FormData extends VoucherDetailsPageFormData { value: number; @@ -63,8 +62,6 @@ const VoucherCreatePage: React.FC = ({ const intl = useIntl(); const navigate = useNavigator(); - const voucherCodesFlag = useFlag("voucher_codes"); - const { makeChangeHandler: makeMetadataChangeHandler } = useMetadataChangeTrigger(); @@ -103,6 +100,7 @@ const VoucherCreatePage: React.FC = ({ prefix, }: GenerateMultipleVoucherCodeFormData) => { clearRowSelection(); + triggerChange(true); set({ codes: [...generateMultipleIds(quantity, prefix), ...data.codes], }); @@ -116,6 +114,10 @@ const VoucherCreatePage: React.FC = ({ }; const handleGenerateCustomCode = (code: string) => { + if (voucherCodeExists(code, data.codes)) { + throw new Error("Code already exists"); + } + triggerChange(true); set({ codes: [{ code }, ...data.codes], }); @@ -141,21 +143,18 @@ const VoucherCreatePage: React.FC = ({ errors={errors} disabled={disabled} onChange={event => handleDiscountTypeChange(data, event)} - variant="create" /> - {voucherCodesFlag.enabled && ( - - )} + { return Array.from({ length: Number(quantity) }).map(() => ({ code: prefix ? `${prefix}-${uuidv4()}` : uuidv4(), status: "Draft", })); }; + +export const voucherCodeExists = ( + code: string, + voucherCodes: VoucherCode[], +) => { + return voucherCodes.some(voucherCode => voucherCode.code === code); +}; diff --git a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx index 6ce6deb921e..3d24c06b4c0 100644 --- a/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx +++ b/src/discounts/components/VoucherDetailsPage/VoucherDetailsPage.tsx @@ -21,7 +21,6 @@ import { RequirementsPicker, } from "@dashboard/discounts/types"; import { voucherListUrl } from "@dashboard/discounts/urls"; -import { useFlag } from "@dashboard/featureFlags"; import { DiscountErrorFragment, DiscountValueTypeEnum, @@ -69,7 +68,7 @@ export interface VoucherDetailsPageFormData extends MetadataFormData { applyOncePerOrder: boolean; onlyForStaff: boolean; channelListings: ChannelVoucherData[]; - code: string; + name: string; discountType: DiscountTypeEnum; endDate: string; endTime: string; @@ -175,7 +174,6 @@ const VoucherDetailsPage: React.FC = ({ }) => { const intl = useIntl(); const navigate = useNavigator(); - const voucherCodesFlag = useFlag("voucher_codes"); const [localErrors, setLocalErrors] = React.useState( [], @@ -207,7 +205,7 @@ const VoucherDetailsPage: React.FC = ({ applyOncePerOrder: voucher?.applyOncePerOrder || false, onlyForStaff: voucher?.onlyForStaff || false, channelListings, - code: voucher?.code || "", + name: voucher?.name || "", discountType, codes: addedVoucherCodes, endDate: splitDateTime(voucher?.endDate ?? "").date, @@ -222,7 +220,7 @@ const VoucherDetailsPage: React.FC = ({ type: voucher?.type ?? VoucherTypeEnum.ENTIRE_ORDER, usageLimit: voucher?.usageLimit ?? 1, used: voucher?.used ?? 0, - singleUse: false, + singleUse: voucher?.singleUse ?? false, metadata: voucher?.metadata.map(mapMetadataItemToInput), privateMetadata: voucher?.privateMetadata.map(mapMetadataItemToInput), }; @@ -252,24 +250,26 @@ const VoucherDetailsPage: React.FC = ({ disabled={disabled} errors={errors} onChange={change} - variant="update" /> - {voucherCodesFlag.enabled && ( - - )} - + { + triggerChange(); + onMultipleVoucheCodesGenerate(codes); + }} + onCustomCodeGenerate={code => { + triggerChange(); + onCustomVoucherCodeGenerate(code); + }} + disabled={disabled} + codes={voucherCodes} + voucherCodesPagination={voucherCodesPagination} + onSettingsChange={onVoucherCodesSettingsChange} + settings={voucherCodesSettings} + /> void; } @@ -23,53 +20,33 @@ const VoucherInfo = ({ data, disabled, errors, - variant, onChange, }: VoucherInfoProps) => { const intl = useIntl(); - const formErrors = getFormErrors(["code"], errors); - - const onGenerateCode = () => - onChange({ - target: { - name: "code", - value: generateCode(10), - }, - }); + const formErrors = getFormErrors(["name"], errors); return ( - - - - - ) - } - /> - - + + {intl.formatMessage(commonMessages.generalInformations)} + + + - - + + ); }; export default VoucherInfo; diff --git a/src/discounts/components/VoucherLimits/VoucherLimits.tsx b/src/discounts/components/VoucherLimits/VoucherLimits.tsx index d9f2e7276b6..424c306420f 100644 --- a/src/discounts/components/VoucherLimits/VoucherLimits.tsx +++ b/src/discounts/components/VoucherLimits/VoucherLimits.tsx @@ -1,7 +1,6 @@ import CardTitle from "@dashboard/components/CardTitle"; import { ControlledCheckbox } from "@dashboard/components/ControlledCheckbox"; import { Grid } from "@dashboard/components/Grid"; -import { useFlag } from "@dashboard/featureFlags"; import { DiscountErrorFragment } from "@dashboard/graphql"; import { getFormErrors } from "@dashboard/utils/errors"; import getDiscountErrorMessage from "@dashboard/utils/errors/discounts"; @@ -35,8 +34,6 @@ const VoucherLimits = ({ const intl = useIntl(); const classes = useStyles(); - const voucherCodesFlag = useFlag("voucher_codes"); - const formErrors = getFormErrors(["usageLimit"], errors); const usesLeft = data.usageLimit - data.used; @@ -113,15 +110,14 @@ const VoucherLimits = ({ name={"onlyForStaff" as keyof VoucherDetailsPageFormData} onChange={onChange} /> - {voucherCodesFlag.enabled && ( - - )} + + ); diff --git a/src/discounts/components/VoucherList/VoucherList.tsx b/src/discounts/components/VoucherList/VoucherList.tsx deleted file mode 100644 index 3437beb8853..00000000000 --- a/src/discounts/components/VoucherList/VoucherList.tsx +++ /dev/null @@ -1,348 +0,0 @@ -// @ts-strict-ignore -import Checkbox from "@dashboard/components/Checkbox"; -import Date from "@dashboard/components/Date"; -import Money from "@dashboard/components/Money"; -import Percent from "@dashboard/components/Percent"; -import ResponsiveTable from "@dashboard/components/ResponsiveTable"; -import Skeleton from "@dashboard/components/Skeleton"; -import TableCellHeader from "@dashboard/components/TableCellHeader"; -import TableHead from "@dashboard/components/TableHead"; -import { TablePaginationWithContext } from "@dashboard/components/TablePagination"; -import TableRowLink from "@dashboard/components/TableRowLink"; -import TooltipTableCellHeader from "@dashboard/components/TooltipTableCellHeader"; -import { commonTooltipMessages } from "@dashboard/components/TooltipTableCellHeader/messages"; -import { VoucherListUrlSortField, voucherUrl } from "@dashboard/discounts/urls"; -import { canBeSorted } from "@dashboard/discounts/views/VoucherList/sort"; -import { DiscountValueTypeEnum, VoucherFragment } from "@dashboard/graphql"; -import { maybe, renderCollection } from "@dashboard/misc"; -import { - ChannelProps, - ListActions, - ListProps, - SortPage, -} from "@dashboard/types"; -import { getArrowDirection } from "@dashboard/utils/sort"; -import { TableBody, TableCell, TableFooter } from "@material-ui/core"; -import { makeStyles } from "@saleor/macaw-ui"; -import clsx from "clsx"; -import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; - -export interface VoucherListProps - extends ListProps, - ListActions, - SortPage, - ChannelProps { - vouchers: VoucherFragment[]; -} - -const useStyles = makeStyles( - theme => ({ - [theme.breakpoints.up("lg")]: { - colEnd: { - width: 180, - }, - colMinSpent: { - width: 150, - }, - colName: {}, - colStart: { - width: 180, - }, - colUses: { - width: 150, - }, - colValue: { - width: 150, - }, - }, - colEnd: { - textAlign: "right", - }, - colMinSpent: { - textAlign: "right", - }, - colName: { - paddingLeft: 0, - }, - colStart: { - textAlign: "right", - }, - colUses: { - textAlign: "right", - }, - colValue: { - textAlign: "right", - }, - tableRow: { - cursor: "pointer", - }, - textRight: { - textAlign: "right", - }, - textOverflow: { - textOverflow: "ellipsis", - overflow: "hidden", - }, - }), - { name: "VoucherList" }, -); - -const numberOfColumns = 7; - -const VoucherList: React.FC = props => { - const { - settings, - disabled, - onUpdateListSettings, - onSort, - vouchers, - isChecked, - selected, - selectedChannelId, - sort, - toggle, - toggleAll, - toolbar, - filterDependency, - } = props; - - const classes = useStyles(props); - const intl = useIntl(); - - return ( - - - onSort(VoucherListUrlSortField.code)} - className={classes.colName} - > - - - onSort(VoucherListUrlSortField.minSpent)} - disabled={ - !canBeSorted(VoucherListUrlSortField.minSpent, !!selectedChannelId) - } - className={classes.colMinSpent} - tooltip={intl.formatMessage(commonTooltipMessages.noFilterSelected, { - filterName: filterDependency.label, - })} - > - - - onSort(VoucherListUrlSortField.startDate)} - className={classes.colStart} - > - - - onSort(VoucherListUrlSortField.endDate)} - className={classes.colEnd} - > - - - onSort(VoucherListUrlSortField.value)} - disabled={ - !canBeSorted(VoucherListUrlSortField.minSpent, !!selectedChannelId) - } - className={classes.colValue} - tooltip={intl.formatMessage(commonTooltipMessages.noFilterSelected, { - filterName: filterDependency.label, - })} - > - - - onSort(VoucherListUrlSortField.limit)} - className={classes.colUses} - > - - - - - - - - - - {renderCollection( - vouchers, - voucher => { - const isSelected = voucher ? isChecked(voucher.id) : false; - const channel = voucher?.channelListings?.find( - listing => listing.channel.id === selectedChannelId, - ); - const hasChannelsLoaded = voucher?.channelListings?.length; - - return ( - - - toggle(voucher.id)} - /> - - - {voucher?.code ?? } - - - {voucher?.code ? ( - hasChannelsLoaded ? ( - - ) : ( - "-" - ) - ) : ( - - )} - - - {voucher?.startDate ? ( - - ) : ( - - )} - - - {voucher?.endDate ? ( - - ) : voucher && voucher.endDate === null ? ( - "-" - ) : ( - - )} - - - {voucher?.code ? ( - hasChannelsLoaded ? ( - voucher.discountValueType === - DiscountValueTypeEnum.FIXED ? ( - - ) : ( - - ) - ) : ( - "-" - ) - ) : ( - - )} - - - {maybe( - () => - voucher.usageLimit === null ? "-" : voucher.usageLimit, - , - )} - - - ); - }, - () => ( - - - - - - ), - )} - - - ); -}; -VoucherList.displayName = "VoucherList"; -export default VoucherList; diff --git a/src/discounts/components/VoucherList/index.ts b/src/discounts/components/VoucherList/index.ts deleted file mode 100644 index c8440963296..00000000000 --- a/src/discounts/components/VoucherList/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from "./VoucherList"; -export * from "./VoucherList"; diff --git a/src/discounts/components/VoucherListDatagrid/datagrid.ts b/src/discounts/components/VoucherListDatagrid/datagrid.ts index 5ace1ebf105..c0634e5d520 100644 --- a/src/discounts/components/VoucherListDatagrid/datagrid.ts +++ b/src/discounts/components/VoucherListDatagrid/datagrid.ts @@ -23,7 +23,7 @@ export const vouchersListStaticColumnsAdapter = ( [ { id: "code", - title: intl.formatMessage(columnsMessages.code), + title: intl.formatMessage(columnsMessages.voucher), width: 350, }, { @@ -83,9 +83,9 @@ export const createGetCellContent = switch (columnId) { case "code": - return readonlyTextCell(rowData?.name ?? rowData?.code ?? PLACEHOLDER); + return readonlyTextCell(rowData?.name ?? PLACEHOLDER); case "min-spent": - return rowData?.code && hasChannelsLoaded + return hasChannelsLoaded ? moneyCell( channel?.minSpent?.amount ?? null, channel?.minSpent?.currency ?? "", diff --git a/src/discounts/components/VoucherListDatagrid/messages.ts b/src/discounts/components/VoucherListDatagrid/messages.ts index bf594a33394..b610b9edd4a 100644 --- a/src/discounts/components/VoucherListDatagrid/messages.ts +++ b/src/discounts/components/VoucherListDatagrid/messages.ts @@ -1,10 +1,9 @@ import { defineMessages } from "react-intl"; export const columnsMessages = defineMessages({ - code: { - id: "JsPIOX", - defaultMessage: "Code", - description: "voucher code", + voucher: { + id: "9BvYb9", + defaultMessage: "Voucher", }, minSpent: { id: "tuYPlG", diff --git a/src/discounts/components/VoucherSummary/VoucherSummary.tsx b/src/discounts/components/VoucherSummary/VoucherSummary.tsx index 33adec39ec5..f8ff103bd21 100644 --- a/src/discounts/components/VoucherSummary/VoucherSummary.tsx +++ b/src/discounts/components/VoucherSummary/VoucherSummary.tsx @@ -18,7 +18,6 @@ import { FormattedMessage, useIntl } from "react-intl"; import { maybe } from "../../../misc"; import { translateVoucherTypes } from "../../translations"; -import useStyles from "./styles"; export interface VoucherSummaryProps extends ChannelProps { voucher: VoucherDetailsFragment; @@ -29,7 +28,6 @@ const VoucherSummary: React.FC = ({ voucher, }) => { const intl = useIntl(); - const classes = useStyles(); const translatedVoucherTypes = translateVoucherTypes(intl); const channel = voucher?.channelListings?.find( @@ -40,18 +38,6 @@ const VoucherSummary: React.FC = ({ - - - - - {maybe(() => voucher.code, )} - - - ["result"]; +export type SearchCollectionOpts = ReturnType< + typeof useCollectionSearch +>["result"]; +export type SearchProductsOpts = ReturnType["result"]; diff --git a/src/discounts/utils.ts b/src/discounts/utils.ts new file mode 100644 index 00000000000..c369aa5736d --- /dev/null +++ b/src/discounts/utils.ts @@ -0,0 +1,104 @@ +import { SaleDetailsQuery, VoucherDetailsQuery } from "@dashboard/graphql"; +import { mapEdgesToItems } from "@dashboard/utils/maps"; + +import { + SearchCategoriesOpts, + SearchCollectionOpts, + SearchProductsOpts, +} from "./types"; + +type SaleOrVoucherData = SaleDetailsQuery | VoucherDetailsQuery; + +const getCriteria = (data: SaleOrVoucherData) => { + if (!!data && "sale" in data) { + return data.sale; + } + if (!!data && "voucher" in data) { + return data.voucher; + } +}; + +export function getFilteredCategories( + data: SaleOrVoucherData, + searchCategoriesOpts: SearchCategoriesOpts, +) { + const categories = mapEdgesToItems(searchCategoriesOpts?.data?.search); + + const criteria = getCriteria(data); + + if (!criteria?.categories?.edges) { + return categories; + } + + const excludedCategoryIds = criteria.categories.edges.map( + category => category.node.id, + ); + + return categories?.filter( + suggestedCategory => !excludedCategoryIds.includes(suggestedCategory.id), + ); +} + +export function getFilteredCollections( + data: SaleOrVoucherData, + searchCollectionsOpts: SearchCollectionOpts, +) { + const collections = mapEdgesToItems(searchCollectionsOpts?.data?.search); + + const criteria = getCriteria(data); + + if (!criteria?.collections?.edges) { + return collections; + } + + const excludedCollectionIds = criteria?.collections.edges.map( + collection => collection.node.id, + ); + + return collections?.filter( + suggestedCollection => + !excludedCollectionIds.includes(suggestedCollection.id), + ); +} + +export function getFilteredProducts( + data: SaleOrVoucherData, + searchProductsOpts: SearchProductsOpts, +) { + const products = mapEdgesToItems(searchProductsOpts?.data?.search); + + const criteria = getCriteria(data); + + if (!criteria?.products?.edges) { + return products; + } + + const excludedProductIds = criteria?.products.edges.map( + product => product.node.id, + ); + + return products?.filter( + suggestedProduct => !excludedProductIds.includes(suggestedProduct.id), + ); +} + +export function getFilteredProductVariants( + data: SaleDetailsQuery, + searchProductsOpts: SearchProductsOpts, +) { + const products = mapEdgesToItems(searchProductsOpts?.data?.search); + + if (!data?.sale?.variants?.edges) { + return products; + } + const excludedVariantsIds = data?.sale?.variants.edges.map( + variant => variant.node.id, + ); + + return products?.map(suggestedProduct => ({ + ...suggestedProduct, + variants: suggestedProduct.variants?.filter( + variant => !excludedVariantsIds.includes(variant.id), + ), + })); +} diff --git a/src/discounts/views/SaleDetails/SaleDetails.tsx b/src/discounts/views/SaleDetails/SaleDetails.tsx index c8ef9457f13..55dc7cb9af7 100644 --- a/src/discounts/views/SaleDetails/SaleDetails.tsx +++ b/src/discounts/views/SaleDetails/SaleDetails.tsx @@ -24,6 +24,12 @@ import { SaleUrlDialog, SaleUrlQueryParams, } from "@dashboard/discounts/urls"; +import { + getFilteredCategories, + getFilteredCollections, + getFilteredProducts, + getFilteredProductVariants, +} from "@dashboard/discounts/utils"; import { SaleDetailsQueryVariables, useSaleCataloguesAddMutation, @@ -50,7 +56,6 @@ import useCollectionSearch from "@dashboard/searches/useCollectionSearch"; import useProductSearch from "@dashboard/searches/useProductSearch"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler"; -import { mapEdgesToItems } from "@dashboard/utils/maps"; import { DialogContentText } from "@material-ui/core"; import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -419,9 +424,7 @@ export const SaleDetails: React.FC = ({ id, params }) => { }, }) } - products={mapEdgesToItems(searchProductsOpts?.data?.search)?.filter( - suggestedProduct => suggestedProduct.id, - )} + products={getFilteredProductVariants(data, searchProductsOpts)} /> = ({ id, params }) => { }, }) } - products={mapEdgesToItems(searchProductsOpts?.data?.search)?.filter( - suggestedProduct => suggestedProduct.id, - )} + products={getFilteredProducts(data, searchProductsOpts)} /> suggestedCategory.id, - )} + categories={getFilteredCategories(data, searchCategoriesOpts)} confirmButtonState={saleCataloguesAddOpts.status} hasMore={searchCategoriesOpts.data?.search.pageInfo.hasNextPage} open={params.action === "assign-category"} @@ -472,9 +471,7 @@ export const SaleDetails: React.FC = ({ id, params }) => { } /> suggestedCategory.id)} + collections={getFilteredCollections(data, searchCollectionsOpts)} confirmButtonState={saleCataloguesAddOpts.status} hasMore={searchCollectionsOpts.data?.search.pageInfo.hasNextPage} open={params.action === "assign-collection"} diff --git a/src/discounts/views/VoucherCreate/VoucherCreate.tsx b/src/discounts/views/VoucherCreate/VoucherCreate.tsx index 76e34538d80..d1f3746ff3c 100644 --- a/src/discounts/views/VoucherCreate/VoucherCreate.tsx +++ b/src/discounts/views/VoucherCreate/VoucherCreate.tsx @@ -6,6 +6,7 @@ import { import useAppChannel from "@dashboard/components/AppLayout/AppChannelContext"; import ChannelsAvailabilityDialog from "@dashboard/components/ChannelsAvailabilityDialog"; import { WindowTitle } from "@dashboard/components/WindowTitle"; +import { VoucherDetailsPageFormData } from "@dashboard/discounts/components/VoucherDetailsPage"; import { useUpdateMetadataMutation, useUpdatePrivateMetadataMutation, @@ -87,9 +88,25 @@ export const VoucherCreateView: React.FC = ({ params }) => { }, }); + const handleFormValidate = (data: VoucherDetailsPageFormData) => { + if (data.codes.length === 0) { + notify({ + status: "error", + text: intl.formatMessage({ + id: "GTCg9O", + defaultMessage: "You must add at least one voucher code", + }), + }); + return false; + } + + return true; + }; + const handleCreate = createHandler( variables => voucherCreate({ variables }), updateChannels, + handleFormValidate, ); const handleSubmit = createMetadataCreateHandler( handleCreate, diff --git a/src/discounts/views/VoucherCreate/handlers.ts b/src/discounts/views/VoucherCreate/handlers.ts index 1f24361fc6c..fa6595dd175 100644 --- a/src/discounts/views/VoucherCreate/handlers.ts +++ b/src/discounts/views/VoucherCreate/handlers.ts @@ -27,14 +27,20 @@ export function createHandler( updateChannels: (options: { variables: VoucherChannelListingUpdateMutationVariables; }) => Promise>, + validateFn: (data: VoucherDetailsPageFormData) => boolean, ) { return async (formData: VoucherDetailsPageFormData) => { + if (!validateFn(formData)) { + return { errors: ["Invalid data"] }; + } + const response = await voucherCreate({ input: { + name: formData.name, applyOncePerCustomer: formData.applyOncePerCustomer, applyOncePerOrder: formData.applyOncePerOrder, onlyForStaff: formData.onlyForStaff, - code: formData.code, + addCodes: formData.codes.map(({ code }) => code).reverse(), discountValueType: formData.discountType === DiscountTypeEnum.VALUE_PERCENTAGE ? DiscountValueTypeEnum.PERCENTAGE @@ -54,6 +60,7 @@ export function createHandler( ? VoucherTypeEnum.SHIPPING : formData.type, usageLimit: formData.hasUsageLimit ? formData.usageLimit : null, + singleUse: formData.singleUse, }, }); diff --git a/src/discounts/views/VoucherDetails/VoucherDetails.tsx b/src/discounts/views/VoucherDetails/VoucherDetails.tsx index 712a9f8d455..bb152c9ad46 100644 --- a/src/discounts/views/VoucherDetails/VoucherDetails.tsx +++ b/src/discounts/views/VoucherDetails/VoucherDetails.tsx @@ -24,6 +24,11 @@ import { VoucherUrlDialog, VoucherUrlQueryParams, } from "@dashboard/discounts/urls"; +import { + getFilteredCategories, + getFilteredCollections, + getFilteredProducts, +} from "@dashboard/discounts/utils"; import { useUpdateMetadataMutation, useUpdatePrivateMetadataMutation, @@ -50,7 +55,6 @@ import useCollectionSearch from "@dashboard/searches/useCollectionSearch"; import useProductSearch from "@dashboard/searches/useProductSearch"; import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers"; import createMetadataUpdateHandler from "@dashboard/utils/handlers/metadataUpdateHandler"; -import { mapEdgesToItems } from "@dashboard/utils/maps"; import { DialogContentText } from "@material-ui/core"; import React, { useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -138,12 +142,14 @@ export const VoucherDetails: React.FC = ({ voucherCodesSettings, selectedVoucherCodesIds, addedVoucherCodes, + voucherCodesRefetch, handleSetSelectedVoucherCodesIds, updateVoucherCodesListSettings, handleAddVoucherCode, handleGenerateMultipleCodes, handleDeleteVoucherCodes, - } = useVoucherCodes(); + handleClearAddedVoucherCodes, + } = useVoucherCodes({ id }); const [openModal, closeModal] = createDialogActionHandlers< VoucherUrlDialog, @@ -196,6 +202,8 @@ export const VoucherDetails: React.FC = ({ if (data.voucherUpdate.errors.length === 0) { closeModal(); notifySaved(); + handleClearAddedVoucherCodes(); + voucherCodesRefetch(); } }, }); @@ -438,9 +446,7 @@ export const VoucherDetails: React.FC = ({ toggleAll={toggleAll} /> suggestedCategory.id, - )} + categories={getFilteredCategories(data, searchCategoriesOpts)} confirmButtonState={voucherCataloguesAddOpts.status} hasMore={searchCategoriesOpts.data?.search.pageInfo.hasNextPage} open={params.action === "assign-category"} @@ -462,9 +468,7 @@ export const VoucherDetails: React.FC = ({ } /> suggestedCategory.id)} + collections={getFilteredCollections(data, searchCollectionsOpts)} confirmButtonState={voucherCataloguesAddOpts.status} hasMore={searchCollectionsOpts.data?.search.pageInfo.hasNextPage} open={params.action === "assign-collection"} @@ -525,9 +529,7 @@ export const VoucherDetails: React.FC = ({ }, }) } - products={mapEdgesToItems(searchProductsOpts?.data?.search)?.filter( - suggestedProduct => suggestedProduct.id, - )} + products={getFilteredProducts(data, searchProductsOpts)} /> = ({ description="dialog content" values={{ voucherCode: ( - {maybe(() => data.voucher.code, "...")} + {maybe(() => data.voucher.name, "...")} ), }} /> diff --git a/src/discounts/views/VoucherDetails/handlers.ts b/src/discounts/views/VoucherDetails/handlers.ts index 32580c87043..23c303433b7 100644 --- a/src/discounts/views/VoucherDetails/handlers.ts +++ b/src/discounts/views/VoucherDetails/handlers.ts @@ -35,6 +35,7 @@ export function createUpdateHandler( updateVoucher({ id, input: { + name: formData.name, applyOncePerCustomer: formData.applyOncePerCustomer, applyOncePerOrder: formData.applyOncePerOrder, onlyForStaff: formData.onlyForStaff, @@ -57,6 +58,8 @@ export function createUpdateHandler( ? VoucherTypeEnum.SHIPPING : formData.type, usageLimit: formData.hasUsageLimit ? formData.usageLimit : null, + singleUse: formData.singleUse, + addCodes: formData.codes.map(({ code }) => code), }, }).then(({ data }) => data?.voucherUpdate.errors ?? []), diff --git a/src/discounts/views/VoucherDetails/hooks/useVoucherCodes.test.ts b/src/discounts/views/VoucherDetails/hooks/useVoucherCodes.test.ts index 13b16e8ca6c..c2861a5bd2f 100644 --- a/src/discounts/views/VoucherDetails/hooks/useVoucherCodes.test.ts +++ b/src/discounts/views/VoucherDetails/hooks/useVoucherCodes.test.ts @@ -1,19 +1,44 @@ +import { useVoucherCodesQuery } from "@dashboard/graphql"; import { act } from "@testing-library/react"; import { renderHook } from "@testing-library/react-hooks"; import { useVoucherCodes } from "./useVoucherCodes"; +const apiVoucherCodes = Array.from({ length: 10 }, (_, i) => ({ + node: { + code: `code ${i + 1}`, + used: 0, + }, +})); + const autoGeneratedVoucherCodes = Array.from({ length: 5 }, () => ({ - code: "code-68276b31-3b41-4004-acd6-bad8c36d524f", + code: "code-123456789", status: "Draft", })); -jest.mock("uuid", () => ({ v4: () => "68276b31-3b41-4004-acd6-bad8c36d524f" })); +jest.mock("uuid", () => ({ v4: () => "123456789" })); +jest.mock("@dashboard/graphql", () => ({ + useVoucherCodesQuery: jest.fn(() => ({ + data: { + voucher: { + codes: { + edges: apiVoucherCodes.slice(0, 2), + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + }, + }, + }, + }, + + refetch: jest.fn(), + })), +})); describe("useVoucherCodes", () => { - it("should return manually generated voucher codes", () => { + it("should return manually generated voucher coded", () => { // Arrange - const { result } = renderHook(() => useVoucherCodes()); + const { result } = renderHook(() => useVoucherCodes({ id: "1" })); // Act act(() => { @@ -25,12 +50,14 @@ describe("useVoucherCodes", () => { expect(result.current.voucherCodes).toEqual([ { code: "code 4", status: "Draft" }, { code: "code 3", status: "Draft" }, + { code: "code 1", used: 0 }, + { code: "code 2", used: 0 }, ]); }); it("should return automatictlly genereted voucher codes", () => { // Arrange - const { result } = renderHook(() => useVoucherCodes()); + const { result } = renderHook(() => useVoucherCodes({ id: "1" })); // Act act(() => { @@ -41,12 +68,118 @@ describe("useVoucherCodes", () => { }); // Assert - expect(result.current.voucherCodes).toEqual([...autoGeneratedVoucherCodes]); + expect(result.current.voucherCodes).toEqual([ + ...autoGeneratedVoucherCodes, + { code: "code 1", used: 0 }, + { code: "code 2", used: 0 }, + ]); + }); + + it("should allow to paginate voucher codes comes from server", () => { + // Arrange + (useVoucherCodesQuery as jest.Mock).mockImplementation(() => ({ + data: { + voucher: { + codes: { + edges: apiVoucherCodes.slice(0, 2), + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + }, + }, + }, + }, + })); + + const { result } = renderHook(() => useVoucherCodes({ id: "1" })); + + // Act + act(() => { + result.current.updateVoucherCodesListSettings("rowNumber", 2); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + false, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + true, + ); + + expect(result.current.voucherCodes).toEqual([ + { code: "code 1", used: 0 }, + { code: "code 2", used: 0 }, + ]); + + // Arrange + (useVoucherCodesQuery as jest.Mock).mockImplementation(() => ({ + data: { + voucher: { + codes: { + edges: apiVoucherCodes.slice(2, 4), + pageInfo: { + hasNextPage: false, + hasPreviousPage: true, + }, + }, + }, + }, + })); + + // Act + act(() => { + result.current.voucherCodesPagination.loadNextPage(); + }); + + // Assert + expect(result.current.voucherCodes).toEqual([ + { code: "code 3", used: 0 }, + { code: "code 4", used: 0 }, + ]); + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + false, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + true, + ); + + // Arrange + (useVoucherCodesQuery as jest.Mock).mockImplementation(() => ({ + data: { + voucher: { + codes: { + edges: apiVoucherCodes.slice(0, 2), + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + }, + }, + }, + }, + })); + + // Act + act(() => { + result.current.voucherCodesPagination.loadPreviousPage(); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + true, + ); + expect(result.current.voucherCodes).toEqual([ + { code: "code 1", used: 0 }, + { code: "code 2", used: 0 }, + ]); }); it("should allow to paginate voucher codes comes from client", () => { // Arrange - const { result } = renderHook(() => useVoucherCodes()); + (useVoucherCodesQuery as jest.Mock).mockImplementation(() => ({ + data: null, + })); + + const { result } = renderHook(() => useVoucherCodes({ id: "1" })); // Act act(() => { @@ -137,4 +270,227 @@ describe("useVoucherCodes", () => { ...autoGeneratedVoucherCodes, ]); }); + + it("should allow to paginate voucher codes comes from client and server", () => { + // Arrange + (useVoucherCodesQuery as jest.Mock).mockImplementation(() => ({ + data: { + voucher: { + codes: { + edges: apiVoucherCodes.slice(0, 2), + pageInfo: { + hasNextPage: false, + hasPreviousPage: true, + }, + }, + }, + }, + })); + + const { result } = renderHook(() => useVoucherCodes({ id: "1" })); + + // Act + act(() => { + result.current.updateVoucherCodesListSettings("rowNumber", 2); + result.current.handleGenerateMultipleCodes({ + quantity: "3", + prefix: "code", + }); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + true, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + false, + ); + expect(result.current.voucherCodes).toEqual( + autoGeneratedVoucherCodes.slice(3), + ); + + // Arrange + (useVoucherCodesQuery as jest.Mock).mockImplementation(() => ({ + data: { + voucher: { + codes: { + edges: [apiVoucherCodes[0]], + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + }, + }, + }, + }, + })); + + // Act + act(() => { + result.current.voucherCodesPagination.loadNextPage(); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + true, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + true, + ); + expect(result.current.voucherCodes).toEqual([ + ...autoGeneratedVoucherCodes.slice(0, 1), + { code: "code 1", used: 0 }, + ]); + + // Arrange + (useVoucherCodesQuery as jest.Mock).mockImplementation(() => ({ + data: { + voucher: { + codes: { + edges: [apiVoucherCodes[1]], + pageInfo: { + hasNextPage: false, + hasPreviousPage: true, + }, + }, + }, + }, + })); + + // Act + act(() => { + result.current.voucherCodesPagination.loadNextPage(); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + false, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + true, + ); + expect(result.current.voucherCodes).toEqual([{ code: "code 2", used: 0 }]); + + // Arrange + (useVoucherCodesQuery as jest.Mock).mockImplementation(() => ({ + data: { + voucher: { + codes: { + edges: [apiVoucherCodes[0]], + pageInfo: { + hasNextPage: true, + hasPreviousPage: false, + }, + }, + }, + }, + })); + + // Act + act(() => { + result.current.voucherCodesPagination.loadPreviousPage(); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + true, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + true, + ); + + expect(result.current.voucherCodes).toEqual([ + ...autoGeneratedVoucherCodes.slice(0, 1), + { code: "code 1", used: 0 }, + ]); + + // Act + act(() => { + result.current.voucherCodesPagination.loadPreviousPage(); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + true, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + false, + ); + expect(result.current.voucherCodes).toEqual( + autoGeneratedVoucherCodes.slice(0, 2), + ); + }); + + it("should allow to handle client and server pagination when client generate whole page", () => { + // Arrange + (useVoucherCodesQuery as jest.Mock).mockImplementation(() => ({ + data: { + voucher: { + codes: { + edges: apiVoucherCodes.slice(0, 2), + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + }, + }, + }, + }, + })); + + const { result } = renderHook(() => useVoucherCodes({ id: "1" })); + + // Act + act(() => { + result.current.updateVoucherCodesListSettings("rowNumber", 10); + result.current.handleGenerateMultipleCodes({ + quantity: "10", + prefix: "code", + }); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + true, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + false, + ); + expect(result.current.voucherCodes).toEqual([ + ...autoGeneratedVoucherCodes, + ...autoGeneratedVoucherCodes, + ]); + + // Act + act(() => { + result.current.voucherCodesPagination.loadNextPage(); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + false, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + true, + ); + expect(result.current.voucherCodes).toEqual([ + { code: "code 1", used: 0 }, + { code: "code 2", used: 0 }, + ]); + + // Act + act(() => { + result.current.voucherCodesPagination.loadPreviousPage(); + }); + + // Assert + expect(result.current.voucherCodesPagination.pageInfo.hasNextPage).toBe( + true, + ); + expect(result.current.voucherCodesPagination.pageInfo.hasPreviousPage).toBe( + false, + ); + expect(result.current.voucherCodes).toEqual([ + ...autoGeneratedVoucherCodes, + ...autoGeneratedVoucherCodes, + ]); + }); }); diff --git a/src/discounts/views/VoucherDetails/hooks/useVoucherCodes.ts b/src/discounts/views/VoucherDetails/hooks/useVoucherCodes.ts index c5dec99d546..2fd8e793978 100644 --- a/src/discounts/views/VoucherDetails/hooks/useVoucherCodes.ts +++ b/src/discounts/views/VoucherDetails/hooks/useVoucherCodes.ts @@ -1,32 +1,82 @@ import useListSettings from "@dashboard/hooks/useListSettings"; import { ListSettings, ListViews } from "@dashboard/types"; +import { useState } from "react"; +import { getVoucherCodesToDisplay } from "../utils"; import { useVoucherCodesClient } from "./useVoucherCodesClient"; import { useVoucherCodesRowSelection } from "./useVoucherCodesRowSelection"; +import { useVoucherCodesServer } from "./useVoucherCodesServer"; -export const useVoucherCodes = () => { +export const useVoucherCodes = ({ id }: { id: string }) => { const { settings: voucherCodesSettings, updateListSettings: updateVoucherCodesListSettings, } = useListSettings(ListViews.VOUCHER_CODES); + const [isServerPagination, setIsServerPagination] = useState(true); + const { addedVoucherCodes, clientVoucherCodes, clientVoucherCodesPagination, + freeSlotsInClientPagianationPage, handleAddVoucherCode, handleGenerateMultipleCodes, handleDeleteAddedVoucherCodes, + handleClearAddedVoucherCodes, + hasClientPaginationNextPage, + hasClientPaginationPrevPage, onSettingsChange, } = useVoucherCodesClient(voucherCodesSettings, () => { clearRowSelection(); + setIsServerPagination(false); + restartServerPagination(); + }); + + const { + freeSlotsInServerPagianationPage, + hasServerPaginationNextPage, + hasServerPaginationPrevPage, + serverVoucherCodesPagination, + serverVoucherCodes, + voucherCodesLoading, + voucherCodesRefetch, + restartServerPagination, + } = useVoucherCodesServer({ + id, + settings: voucherCodesSettings, + skipFetch: + !isServerPagination && + freeSlotsInClientPagianationPage === 0 && + hasClientPaginationNextPage, + isServerPagination, + paginationState: { + first: + !isServerPagination && freeSlotsInClientPagianationPage > 0 + ? freeSlotsInClientPagianationPage + : voucherCodesSettings.rowNumber, + }, + }); + + const voucherCodes = getVoucherCodesToDisplay({ + clientVoucherCodes, + freeSlotsInClientPagianationPage, + hasClientPaginationNextPage, + freeSlotsInServerPagianationPage, + hasServerPaginationPrevPage, + isServerPagination, + serverVoucherCodes, }); + const voucherCodesPagination = isServerPagination + ? serverVoucherCodesPagination + : clientVoucherCodesPagination; + const { selectedVoucherCodesIds, handleSetSelectedVoucherCodesIds, clearRowSelection, - } = useVoucherCodesRowSelection(clientVoucherCodes); + } = useVoucherCodesRowSelection(voucherCodes); const handleDeleteVoucherCodes = () => { clearRowSelection(); @@ -37,15 +87,90 @@ export const useVoucherCodes = () => { key: keyof ListSettings, value: number | string[], ) => { + if (addedVoucherCodes.length > 0 && isServerPagination) { + setIsServerPagination(false); + } + restartServerPagination(); updateVoucherCodesListSettings(key, value); onSettingsChange(key, value); }; + const handleLoadNextPage = () => { + clearRowSelection(); + + if (isServerPagination) { + serverVoucherCodesPagination.loadNextPage(); + } + + if (!isServerPagination) { + if (!hasClientPaginationNextPage) { + setIsServerPagination(true); + } + + if ( + freeSlotsInClientPagianationPage > 0 && + !hasClientPaginationNextPage + ) { + serverVoucherCodesPagination.loadNextPage(); + } + } + + clientVoucherCodesPagination.loadNextPage(); + }; + + const handleLoadPrevousPage = () => { + clearRowSelection(); + + if (isServerPagination) { + if (hasServerPaginationPrevPage) { + serverVoucherCodesPagination.loadPreviousPage(); + } else { + clientVoucherCodesPagination.loadPreviousPage(); + setIsServerPagination(false); + } + } + + clientVoucherCodesPagination.loadPreviousPage(); + }; + + const calculateHasNextPage = () => { + // In case when client voucher codes takes all slots + // on page and there are some server voucher codes to display + if ( + !isServerPagination && + !hasClientPaginationNextPage && + freeSlotsInClientPagianationPage === 0 && + serverVoucherCodes.length > 0 + ) { + return true; + } + + return hasClientPaginationNextPage || hasServerPaginationNextPage; + }; + + const calculateHasPrevPage = () => { + if (isServerPagination) { + return hasServerPaginationPrevPage || hasClientPaginationPrevPage; + } + + return hasClientPaginationPrevPage; + }; + return { - voucherCodes: clientVoucherCodes, + voucherCodes, addedVoucherCodes, - voucherCodesLoading: false, - voucherCodesPagination: clientVoucherCodesPagination, + voucherCodesLoading, + voucherCodesPagination: { + ...voucherCodesPagination, + pageInfo: { + ...voucherCodesPagination.pageInfo, + hasNextPage: calculateHasNextPage(), + hasPreviousPage: calculateHasPrevPage(), + }, + loadNextPage: handleLoadNextPage, + loadPreviousPage: handleLoadPrevousPage, + }, + voucherCodesRefetch, voucherCodesSettings, updateVoucherCodesListSettings: handleUpdateVoucherCodesListSettings, selectedVoucherCodesIds, @@ -53,5 +178,6 @@ export const useVoucherCodes = () => { handleAddVoucherCode, handleGenerateMultipleCodes, handleDeleteVoucherCodes, + handleClearAddedVoucherCodes, }; }; diff --git a/src/discounts/views/VoucherDetails/hooks/useVoucherCodesClient.ts b/src/discounts/views/VoucherDetails/hooks/useVoucherCodesClient.ts index ac06d8aea9c..175c2b6fb80 100644 --- a/src/discounts/views/VoucherDetails/hooks/useVoucherCodesClient.ts +++ b/src/discounts/views/VoucherDetails/hooks/useVoucherCodesClient.ts @@ -1,7 +1,10 @@ import { VoucherCode } from "@dashboard/discounts/components/VoucherCodesDatagrid/types"; import { GenerateMultipleVoucherCodeFormData } from "@dashboard/discounts/components/VoucherCodesGenerateDialog"; import { useVoucherCodesPagination } from "@dashboard/discounts/components/VoucherCreatePage/hooks/useVoucherCodesPagination"; -import { generateMultipleIds } from "@dashboard/discounts/components/VoucherCreatePage/utils"; +import { + generateMultipleIds, + voucherCodeExists, +} from "@dashboard/discounts/components/VoucherCreatePage/utils"; import { UseListSettings } from "@dashboard/hooks/useListSettings"; import { LocalPagination } from "@dashboard/hooks/useLocalPaginator"; import { ListSettings } from "@dashboard/types"; @@ -17,6 +20,7 @@ interface UseVoucherCodesClient { onSettingsChange: UseListSettings["updateListSettings"]; handleDeleteAddedVoucherCodes: (idsToDelete: string[]) => void; handleAddVoucherCode: (code: string) => void; + handleClearAddedVoucherCodes: () => void; handleGenerateMultipleCodes: ({ quantity, prefix, @@ -45,6 +49,10 @@ export const useVoucherCodesClient = ( settings.rowNumber - clientVoucherCodes.length; const handleAddVoucherCode = (code: string) => { + if (voucherCodeExists(code, addedVoucherCodes)) { + throw new Error("Code already exists"); + } + setAddedVoucherCodes(codes => [{ code, status: "Draft" }, ...codes]); switchToClientPagination(); resetPage(); @@ -68,6 +76,10 @@ export const useVoucherCodesClient = ( ); }; + const handleClearAddedVoucherCodes = () => { + setAddedVoucherCodes([]); + }; + return { addedVoucherCodes, clientVoucherCodes, @@ -79,5 +91,6 @@ export const useVoucherCodesClient = ( handleAddVoucherCode, handleGenerateMultipleCodes, handleDeleteAddedVoucherCodes, + handleClearAddedVoucherCodes, }; }; diff --git a/src/discounts/views/VoucherDetails/hooks/useVoucherCodesServer.tsx b/src/discounts/views/VoucherDetails/hooks/useVoucherCodesServer.tsx new file mode 100644 index 00000000000..ab3e5d61ea0 --- /dev/null +++ b/src/discounts/views/VoucherDetails/hooks/useVoucherCodesServer.tsx @@ -0,0 +1,91 @@ +import { VoucherCode } from "@dashboard/discounts/components/VoucherCodesDatagrid/types"; +import { useVoucherCodesQuery } from "@dashboard/graphql"; +import useLocalPaginator, { + LocalPagination, + PaginationState, + useLocalPaginationState, +} from "@dashboard/hooks/useLocalPaginator"; +import { ListSettings } from "@dashboard/types"; +import { mapEdgesToItems } from "@dashboard/utils/maps"; + +interface UseVoucherCodesServerProps { + settings: ListSettings; + id: string; + skipFetch?: boolean; + isServerPagination?: boolean; + paginationState?: PaginationState; +} + +interface VoucherCodesServer { + voucherCodesLoading: boolean; + voucherCodesRefetch: () => void; + serverVoucherCodesPagination: LocalPagination; + hasServerPaginationNextPage: boolean; + hasServerPaginationPrevPage: boolean; + freeSlotsInServerPagianationPage: number; + serverVoucherCodes: VoucherCode[]; + restartServerPagination: () => void; +} + +export const useVoucherCodesServer = ({ + settings, + id, + skipFetch, + isServerPagination, + paginationState = {}, +}: UseVoucherCodesServerProps): VoucherCodesServer => { + const [ + serverVoucherCodesPaginationState, + setServerVoucherCodesPaginationState, + ] = useLocalPaginationState(settings.rowNumber); + + const serverVoucherCodesPaginate = useLocalPaginator( + setServerVoucherCodesPaginationState, + ); + + const restartServerPagination = () => { + setServerVoucherCodesPaginationState({}); + }; + + const { + data: voucherCodesData, + loading: voucherCodesLoading, + refetch: voucherCodesRefetch, + } = useVoucherCodesQuery({ + skip: skipFetch, + variables: { + id, + ...(!isServerPagination + ? paginationState + : serverVoucherCodesPaginationState), + }, + }); + + const serverVoucherCodesPagination = serverVoucherCodesPaginate( + voucherCodesData?.voucher?.codes?.pageInfo, + serverVoucherCodesPaginationState, + ); + + const hasServerPaginationNextPage = + serverVoucherCodesPagination?.pageInfo?.hasNextPage ?? false; + const hasServerPaginationPrevPage = + serverVoucherCodesPagination?.pageInfo?.hasPreviousPage ?? false; + + const serverVoucherCodes = (mapEdgesToItems( + voucherCodesData?.voucher?.codes, + ) ?? []) as VoucherCode[]; + + const freeSlotsInServerPagianationPage = + settings.rowNumber - serverVoucherCodes.length; + + return { + voucherCodesLoading, + voucherCodesRefetch, + serverVoucherCodes, + serverVoucherCodesPagination, + hasServerPaginationNextPage, + hasServerPaginationPrevPage, + restartServerPagination, + freeSlotsInServerPagianationPage, + }; +}; diff --git a/src/fragments/discounts.ts b/src/fragments/discounts.ts index d830cc424a2..f301f770137 100644 --- a/src/fragments/discounts.ts +++ b/src/fragments/discounts.ts @@ -121,7 +121,6 @@ export const voucherFragment = gql` fragment Voucher on Voucher { ...Metadata id - code name startDate endDate @@ -150,15 +149,23 @@ export const voucherFragment = gql` } `; +export const voucherCodeFragment = gql` + fragment VoucherCode on VoucherCode { + code + used + isActive + } +`; + export const voucherDetailsFragment = gql` fragment VoucherDetails on Voucher { ...Voucher - code usageLimit used applyOncePerOrder applyOncePerCustomer onlyForStaff + singleUse productsCount: products { totalCount } diff --git a/src/fragments/errors.ts b/src/fragments/errors.ts index 2307591b82b..cd676dfb047 100644 --- a/src/fragments/errors.ts +++ b/src/fragments/errors.ts @@ -565,6 +565,12 @@ export const orderGrantRefundCreateErrorFragment = gql` field message code + lines { + field + message + code + lineId + } } `; @@ -573,5 +579,17 @@ export const orderGrantRefundUpdateErrorFragment = gql` field message code + addLines { + field + message + code + lineId + } + removeLines { + field + message + code + lineId + } } `; diff --git a/src/fragments/orders.ts b/src/fragments/orders.ts index d86b5c61c96..b7f749c0af8 100644 --- a/src/fragments/orders.ts +++ b/src/fragments/orders.ts @@ -622,6 +622,24 @@ export const orderLineGrantRefund = gql` } `; +export const orderDetailsGrantedRefund = gql` + fragment OrderDetailsGrantedRefund on OrderGrantedRefund { + id + reason + amount { + ...Money + } + shippingCostsIncluded + lines { + id + quantity + orderLine { + ...OrderLine + } + } + } +`; + export const grantRefundFulfillment = gql` fragment OrderFulfillmentGrantRefund on Fulfillment { id @@ -657,5 +675,8 @@ export const fragmentOrderDetailsGrantRefund = gql` ...Money } } + grantedRefunds { + ...OrderDetailsGrantedRefund + } } `; diff --git a/src/graphql/fragmentTypes.generated.ts b/src/graphql/fragmentTypes.generated.ts index 798d00b2664..45b02875402 100644 --- a/src/graphql/fragmentTypes.generated.ts +++ b/src/graphql/fragmentTypes.generated.ts @@ -169,6 +169,7 @@ "TransactionRefundRequested", "TranslationCreated", "TranslationUpdated", + "VoucherCodeExportCompleted", "VoucherCreated", "VoucherDeleted", "VoucherMetadataUpdated", diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts index a3ee1eb5c69..1cd20eac590 100644 --- a/src/graphql/hooks.generated.ts +++ b/src/graphql/hooks.generated.ts @@ -521,11 +521,17 @@ export const SaleDetailsFragmentDoc = gql` ${SaleFragmentDoc} ${ChannelListingProductWithoutPricingFragmentDoc} ${PageInfoFragmentDoc}`; +export const VoucherCodeFragmentDoc = gql` + fragment VoucherCode on VoucherCode { + code + used + isActive +} + `; export const VoucherFragmentDoc = gql` fragment Voucher on Voucher { ...Metadata id - code name startDate endDate @@ -556,12 +562,12 @@ export const VoucherFragmentDoc = gql` export const VoucherDetailsFragmentDoc = gql` fragment VoucherDetails on Voucher { ...Voucher - code usageLimit used applyOncePerOrder applyOncePerCustomer onlyForStaff + singleUse productsCount: products { totalCount } @@ -1119,6 +1125,12 @@ export const OrderGrantRefundCreateErrorFragmentDoc = gql` field message code + lines { + field + message + code + lineId + } } `; export const OrderGrantRefundUpdateErrorFragmentDoc = gql` @@ -1126,6 +1138,18 @@ export const OrderGrantRefundUpdateErrorFragmentDoc = gql` field message code + addLines { + field + message + code + lineId + } + removeLines { + field + message + code + lineId + } } `; export const GiftCardsSettingsFragmentDoc = gql` @@ -1990,6 +2014,24 @@ export const OrderFulfillmentGrantRefundFragmentDoc = gql` } } ${OrderLineGrantRefundFragmentDoc}`; +export const OrderDetailsGrantedRefundFragmentDoc = gql` + fragment OrderDetailsGrantedRefund on OrderGrantedRefund { + id + reason + amount { + ...Money + } + shippingCostsIncluded + lines { + id + quantity + orderLine { + ...OrderLine + } + } +} + ${MoneyFragmentDoc} +${OrderLineFragmentDoc}`; export const OrderDetailsGrantRefundFragmentDoc = gql` fragment OrderDetailsGrantRefund on Order { id @@ -2010,10 +2052,14 @@ export const OrderDetailsGrantRefundFragmentDoc = gql` ...Money } } + grantedRefunds { + ...OrderDetailsGrantedRefund + } } ${OrderLineGrantRefundFragmentDoc} ${OrderFulfillmentGrantRefundFragmentDoc} -${MoneyFragmentDoc}`; +${MoneyFragmentDoc} +${OrderDetailsGrantedRefundFragmentDoc}`; export const PageTypeFragmentDoc = gql` fragment PageType on PageType { id @@ -7442,6 +7488,7 @@ export const VoucherUpdateDocument = gql` voucherUpdate(id: $id, input: $input) { errors { ...DiscountError + voucherCodes } voucher { ...Voucher @@ -7576,6 +7623,7 @@ export const VoucherCreateDocument = gql` voucherCreate(input: $input) { errors { ...DiscountError + voucherCodes } voucher { ...Voucher @@ -7615,6 +7663,7 @@ export const VoucherDeleteDocument = gql` voucherDelete(id: $id) { errors { ...DiscountError + voucherCodes } } } @@ -7879,6 +7928,55 @@ export function useVoucherDetailsLazyQuery(baseOptions?: ApolloReactHooks.LazyQu export type VoucherDetailsQueryHookResult = ReturnType; export type VoucherDetailsLazyQueryHookResult = ReturnType; export type VoucherDetailsQueryResult = Apollo.QueryResult; +export const VoucherCodesDocument = gql` + query VoucherCodes($id: ID!, $after: String, $before: String, $first: Int, $last: Int) { + voucher(id: $id) { + codes(first: $first, last: $last, before: $before, after: $after) { + edges { + node { + ...VoucherCode + } + } + pageInfo { + ...PageInfo + } + } + } +} + ${VoucherCodeFragmentDoc} +${PageInfoFragmentDoc}`; + +/** + * __useVoucherCodesQuery__ + * + * To run a query within a React component, call `useVoucherCodesQuery` and pass it any options that fit your needs. + * When your component renders, `useVoucherCodesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useVoucherCodesQuery({ + * variables: { + * id: // value for 'id' + * after: // value for 'after' + * before: // value for 'before' + * first: // value for 'first' + * last: // value for 'last' + * }, + * }); + */ +export function useVoucherCodesQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(VoucherCodesDocument, options); + } +export function useVoucherCodesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(VoucherCodesDocument, options); + } +export type VoucherCodesQueryHookResult = ReturnType; +export type VoucherCodesLazyQueryHookResult = ReturnType; +export type VoucherCodesQueryResult = Apollo.QueryResult; export const FileUploadDocument = gql` mutation FileUpload($file: Upload!) { fileUpload(file: $file) { @@ -8745,8 +8843,8 @@ export function useCustomerGiftCardListLazyQuery(baseOptions?: ApolloReactHooks. export type CustomerGiftCardListQueryHookResult = ReturnType; export type CustomerGiftCardListLazyQueryHookResult = ReturnType; export type CustomerGiftCardListQueryResult = Apollo.QueryResult; -export const HomeDocument = gql` - query Home($channel: String!, $datePeriod: DateRangeInput!, $hasPermissionToManageProducts: Boolean!, $hasPermissionToManageOrders: Boolean!) { +export const HomeAnaliticsDocument = gql` + query HomeAnalitics($channel: String!, $datePeriod: DateRangeInput!, $hasPermissionToManageOrders: Boolean!) { salesToday: ordersTotal(period: TODAY, channel: $channel) @include(if: $hasPermissionToManageOrders) { gross { amount @@ -8756,18 +8854,93 @@ export const HomeDocument = gql` ordersToday: orders(filter: {created: $datePeriod}, channel: $channel) @include(if: $hasPermissionToManageOrders) { totalCount } - ordersToFulfill: orders(filter: {status: READY_TO_FULFILL}, channel: $channel) @include(if: $hasPermissionToManageOrders) { - totalCount - } - ordersToCapture: orders(filter: {status: READY_TO_CAPTURE}, channel: $channel) @include(if: $hasPermissionToManageOrders) { - totalCount - } - productsOutOfStock: products( - filter: {stockAvailability: OUT_OF_STOCK} - channel: $channel - ) { - totalCount +} + `; + +/** + * __useHomeAnaliticsQuery__ + * + * To run a query within a React component, call `useHomeAnaliticsQuery` and pass it any options that fit your needs. + * When your component renders, `useHomeAnaliticsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useHomeAnaliticsQuery({ + * variables: { + * channel: // value for 'channel' + * datePeriod: // value for 'datePeriod' + * hasPermissionToManageOrders: // value for 'hasPermissionToManageOrders' + * }, + * }); + */ +export function useHomeAnaliticsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(HomeAnaliticsDocument, options); + } +export function useHomeAnaliticsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(HomeAnaliticsDocument, options); + } +export type HomeAnaliticsQueryHookResult = ReturnType; +export type HomeAnaliticsLazyQueryHookResult = ReturnType; +export type HomeAnaliticsQueryResult = Apollo.QueryResult; +export const HomeActivitiesDocument = gql` + query HomeActivities($hasPermissionToManageOrders: Boolean!) { + activities: homepageEvents(last: 10) @include(if: $hasPermissionToManageOrders) { + edges { + node { + amount + composedId + date + email + emailType + id + message + orderNumber + oversoldItems + quantity + type + user { + id + email + } + } + } } +} + `; + +/** + * __useHomeActivitiesQuery__ + * + * To run a query within a React component, call `useHomeActivitiesQuery` and pass it any options that fit your needs. + * When your component renders, `useHomeActivitiesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useHomeActivitiesQuery({ + * variables: { + * hasPermissionToManageOrders: // value for 'hasPermissionToManageOrders' + * }, + * }); + */ +export function useHomeActivitiesQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(HomeActivitiesDocument, options); + } +export function useHomeActivitiesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(HomeActivitiesDocument, options); + } +export type HomeActivitiesQueryHookResult = ReturnType; +export type HomeActivitiesLazyQueryHookResult = ReturnType; +export type HomeActivitiesQueryResult = Apollo.QueryResult; +export const HomeTopProductsDocument = gql` + query HomeTopProducts($channel: String!, $hasPermissionToManageProducts: Boolean!) { productTopToday: reportProductSales(period: TODAY, first: 5, channel: $channel) @include(if: $hasPermissionToManageProducts) { edges { node { @@ -8795,60 +8968,82 @@ export const HomeDocument = gql` } } } - activities: homepageEvents(last: 10) @include(if: $hasPermissionToManageOrders) { - edges { - node { - amount - composedId - date - email - emailType - id - message - orderNumber - oversoldItems - quantity - type - user { - id - email - } +} + `; + +/** + * __useHomeTopProductsQuery__ + * + * To run a query within a React component, call `useHomeTopProductsQuery` and pass it any options that fit your needs. + * When your component renders, `useHomeTopProductsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useHomeTopProductsQuery({ + * variables: { + * channel: // value for 'channel' + * hasPermissionToManageProducts: // value for 'hasPermissionToManageProducts' + * }, + * }); + */ +export function useHomeTopProductsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useQuery(HomeTopProductsDocument, options); } - } +export function useHomeTopProductsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return ApolloReactHooks.useLazyQuery(HomeTopProductsDocument, options); + } +export type HomeTopProductsQueryHookResult = ReturnType; +export type HomeTopProductsLazyQueryHookResult = ReturnType; +export type HomeTopProductsQueryResult = Apollo.QueryResult; +export const HomeNotificationsDocument = gql` + query homeNotifications($channel: String!, $hasPermissionToManageOrders: Boolean!) { + ordersToFulfill: orders(filter: {status: READY_TO_FULFILL}, channel: $channel) @include(if: $hasPermissionToManageOrders) { + totalCount + } + ordersToCapture: orders(filter: {status: READY_TO_CAPTURE}, channel: $channel) @include(if: $hasPermissionToManageOrders) { + totalCount + } + productsOutOfStock: products( + filter: {stockAvailability: OUT_OF_STOCK} + channel: $channel + ) { + totalCount } } `; /** - * __useHomeQuery__ + * __useHomeNotificationsQuery__ * - * To run a query within a React component, call `useHomeQuery` and pass it any options that fit your needs. - * When your component renders, `useHomeQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useHomeNotificationsQuery` and pass it any options that fit your needs. + * When your component renders, `useHomeNotificationsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useHomeQuery({ + * const { data, loading, error } = useHomeNotificationsQuery({ * variables: { * channel: // value for 'channel' - * datePeriod: // value for 'datePeriod' - * hasPermissionToManageProducts: // value for 'hasPermissionToManageProducts' * hasPermissionToManageOrders: // value for 'hasPermissionToManageOrders' * }, * }); */ -export function useHomeQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { +export function useHomeNotificationsQuery(baseOptions: ApolloReactHooks.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return ApolloReactHooks.useQuery(HomeDocument, options); + return ApolloReactHooks.useQuery(HomeNotificationsDocument, options); } -export function useHomeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { +export function useHomeNotificationsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} - return ApolloReactHooks.useLazyQuery(HomeDocument, options); + return ApolloReactHooks.useLazyQuery(HomeNotificationsDocument, options); } -export type HomeQueryHookResult = ReturnType; -export type HomeLazyQueryHookResult = ReturnType; -export type HomeQueryResult = Apollo.QueryResult; +export type HomeNotificationsQueryHookResult = ReturnType; +export type HomeNotificationsLazyQueryHookResult = ReturnType; +export type HomeNotificationsQueryResult = Apollo.QueryResult; export const MenuCreateDocument = gql` mutation MenuCreate($input: MenuCreateInput!) { menuCreate(input: $input) { @@ -10498,8 +10693,11 @@ export type OrderTransactionRequestActionMutationHookResult = ReturnType; export type OrderTransactionRequestActionMutationOptions = Apollo.BaseMutationOptions; export const OrderGrantRefundAddDocument = gql` - mutation OrderGrantRefundAdd($orderId: ID!, $amount: Decimal!, $reason: String) { - orderGrantRefundCreate(id: $orderId, input: {amount: $amount, reason: $reason}) { + mutation OrderGrantRefundAdd($orderId: ID!, $amount: Decimal, $reason: String, $lines: [OrderGrantRefundCreateLineInput!], $grantRefundForShipping: Boolean) { + orderGrantRefundCreate( + id: $orderId + input: {amount: $amount, reason: $reason, lines: $lines, grantRefundForShipping: $grantRefundForShipping} + ) { errors { ...OrderGrantRefundCreateError } @@ -10524,6 +10722,8 @@ export type OrderGrantRefundAddMutationFn = Apollo.MutationFunction; export type OrderGrantRefundAddMutationOptions = Apollo.BaseMutationOptions; export const OrderGrantRefundEditDocument = gql` - mutation OrderGrantRefundEdit($refundId: ID!, $amount: Decimal!, $reason: String) { - orderGrantRefundUpdate(id: $refundId, input: {amount: $amount, reason: $reason}) { + mutation OrderGrantRefundEdit($refundId: ID!, $amount: Decimal, $reason: String, $addLines: [OrderGrantRefundUpdateLineAddInput!], $removeLines: [ID!], $grantRefundForShipping: Boolean) { + orderGrantRefundUpdate( + id: $refundId + input: {amount: $amount, reason: $reason, addLines: $addLines, removeLines: $removeLines, grantRefundForShipping: $grantRefundForShipping} + ) { errors { ...OrderGrantRefundUpdateError } @@ -10561,6 +10764,9 @@ export type OrderGrantRefundEditMutationFn = Apollo.MutationFunction | FieldReadFunction, version?: FieldPolicy | FieldReadFunction }; -export type CheckoutKeySpecifier = ('authorizeStatus' | 'availableCollectionPoints' | 'availablePaymentGateways' | 'availableShippingMethods' | 'billingAddress' | 'channel' | 'chargeStatus' | 'created' | 'deliveryMethod' | 'discount' | 'discountName' | 'displayGrossPrices' | 'email' | 'giftCards' | 'id' | 'isShippingRequired' | 'languageCode' | 'lastChange' | 'lines' | 'metadata' | 'metafield' | 'metafields' | 'note' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'problems' | 'quantity' | 'shippingAddress' | 'shippingMethod' | 'shippingMethods' | 'shippingPrice' | 'stockReservationExpires' | 'storedPaymentMethods' | 'subtotalPrice' | 'taxExemption' | 'token' | 'totalBalance' | 'totalPrice' | 'transactions' | 'translatedDiscountName' | 'updatedAt' | 'user' | 'voucherCode' | CheckoutKeySpecifier)[]; +export type CheckoutKeySpecifier = ('authorizeStatus' | 'availableCollectionPoints' | 'availablePaymentGateways' | 'availableShippingMethods' | 'billingAddress' | 'channel' | 'chargeStatus' | 'created' | 'deliveryMethod' | 'discount' | 'discountName' | 'displayGrossPrices' | 'email' | 'giftCards' | 'id' | 'isShippingRequired' | 'languageCode' | 'lastChange' | 'lines' | 'metadata' | 'metafield' | 'metafields' | 'note' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'problems' | 'quantity' | 'shippingAddress' | 'shippingMethod' | 'shippingMethods' | 'shippingPrice' | 'stockReservationExpires' | 'storedPaymentMethods' | 'subtotalPrice' | 'taxExemption' | 'token' | 'totalBalance' | 'totalPrice' | 'transactions' | 'translatedDiscountName' | 'updatedAt' | 'user' | 'voucher' | 'voucherCode' | CheckoutKeySpecifier)[]; export type CheckoutFieldPolicy = { authorizeStatus?: FieldPolicy | FieldReadFunction, availableCollectionPoints?: FieldPolicy | FieldReadFunction, @@ -1063,6 +1063,7 @@ export type CheckoutFieldPolicy = { translatedDiscountName?: FieldPolicy | FieldReadFunction, updatedAt?: FieldPolicy | FieldReadFunction, user?: FieldPolicy | FieldReadFunction, + voucher?: FieldPolicy | FieldReadFunction, voucherCode?: FieldPolicy | FieldReadFunction }; export type CheckoutAddPromoCodeKeySpecifier = ('checkout' | 'checkoutErrors' | 'errors' | CheckoutAddPromoCodeKeySpecifier)[]; @@ -1660,13 +1661,14 @@ export type DigitalContentUrlCreateFieldPolicy = { errors?: FieldPolicy | FieldReadFunction, productErrors?: FieldPolicy | FieldReadFunction }; -export type DiscountErrorKeySpecifier = ('channels' | 'code' | 'field' | 'message' | 'products' | DiscountErrorKeySpecifier)[]; +export type DiscountErrorKeySpecifier = ('channels' | 'code' | 'field' | 'message' | 'products' | 'voucherCodes' | DiscountErrorKeySpecifier)[]; export type DiscountErrorFieldPolicy = { channels?: FieldPolicy | FieldReadFunction, code?: FieldPolicy | FieldReadFunction, field?: FieldPolicy | FieldReadFunction, message?: FieldPolicy | FieldReadFunction, - products?: FieldPolicy | FieldReadFunction + products?: FieldPolicy | FieldReadFunction, + voucherCodes?: FieldPolicy | FieldReadFunction }; export type DomainKeySpecifier = ('host' | 'sslEnabled' | 'url' | DomainKeySpecifier)[]; export type DomainFieldPolicy = { @@ -1838,6 +1840,11 @@ export type ExportProductsFieldPolicy = { exportErrors?: FieldPolicy | FieldReadFunction, exportFile?: FieldPolicy | FieldReadFunction }; +export type ExportVoucherCodesKeySpecifier = ('errors' | 'exportFile' | ExportVoucherCodesKeySpecifier)[]; +export type ExportVoucherCodesFieldPolicy = { + errors?: FieldPolicy | FieldReadFunction, + exportFile?: FieldPolicy | FieldReadFunction +}; export type ExternalAuthenticationKeySpecifier = ('id' | 'name' | ExternalAuthenticationKeySpecifier)[]; export type ExternalAuthenticationFieldPolicy = { id?: FieldPolicy | FieldReadFunction, @@ -1902,7 +1909,7 @@ export type FileUploadFieldPolicy = { uploadErrors?: FieldPolicy | FieldReadFunction, uploadedFile?: FieldPolicy | FieldReadFunction }; -export type FulfillmentKeySpecifier = ('created' | 'fulfillmentOrder' | 'id' | 'lines' | 'metadata' | 'metafield' | 'metafields' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'status' | 'statusDisplay' | 'trackingNumber' | 'warehouse' | FulfillmentKeySpecifier)[]; +export type FulfillmentKeySpecifier = ('created' | 'fulfillmentOrder' | 'id' | 'lines' | 'metadata' | 'metafield' | 'metafields' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'shippingRefundedAmount' | 'status' | 'statusDisplay' | 'totalRefundedAmount' | 'trackingNumber' | 'warehouse' | FulfillmentKeySpecifier)[]; export type FulfillmentFieldPolicy = { created?: FieldPolicy | FieldReadFunction, fulfillmentOrder?: FieldPolicy | FieldReadFunction, @@ -1914,8 +1921,10 @@ export type FulfillmentFieldPolicy = { privateMetadata?: FieldPolicy | FieldReadFunction, privateMetafield?: FieldPolicy | FieldReadFunction, privateMetafields?: FieldPolicy | FieldReadFunction, + shippingRefundedAmount?: FieldPolicy | FieldReadFunction, status?: FieldPolicy | FieldReadFunction, statusDisplay?: FieldPolicy | FieldReadFunction, + totalRefundedAmount?: FieldPolicy | FieldReadFunction, trackingNumber?: FieldPolicy | FieldReadFunction, warehouse?: FieldPolicy | FieldReadFunction }; @@ -2616,7 +2625,7 @@ export type MoneyRangeFieldPolicy = { start?: FieldPolicy | FieldReadFunction, stop?: FieldPolicy | FieldReadFunction }; -export type MutationKeySpecifier = ('accountAddressCreate' | 'accountAddressDelete' | 'accountAddressUpdate' | 'accountDelete' | 'accountRegister' | 'accountRequestDeletion' | 'accountSetDefaultAddress' | 'accountUpdate' | 'addressCreate' | 'addressDelete' | 'addressSetDefault' | 'addressUpdate' | 'appActivate' | 'appCreate' | 'appDeactivate' | 'appDelete' | 'appDeleteFailedInstallation' | 'appFetchManifest' | 'appInstall' | 'appRetryInstall' | 'appTokenCreate' | 'appTokenDelete' | 'appTokenVerify' | 'appUpdate' | 'assignNavigation' | 'assignWarehouseShippingZone' | 'attributeBulkCreate' | 'attributeBulkDelete' | 'attributeBulkTranslate' | 'attributeBulkUpdate' | 'attributeCreate' | 'attributeDelete' | 'attributeReorderValues' | 'attributeTranslate' | 'attributeUpdate' | 'attributeValueBulkDelete' | 'attributeValueBulkTranslate' | 'attributeValueCreate' | 'attributeValueDelete' | 'attributeValueTranslate' | 'attributeValueUpdate' | 'categoryBulkDelete' | 'categoryCreate' | 'categoryDelete' | 'categoryTranslate' | 'categoryUpdate' | 'channelActivate' | 'channelCreate' | 'channelDeactivate' | 'channelDelete' | 'channelReorderWarehouses' | 'channelUpdate' | 'checkoutAddPromoCode' | 'checkoutBillingAddressUpdate' | 'checkoutComplete' | 'checkoutCreate' | 'checkoutCreateFromOrder' | 'checkoutCustomerAttach' | 'checkoutCustomerDetach' | 'checkoutDeliveryMethodUpdate' | 'checkoutEmailUpdate' | 'checkoutLanguageCodeUpdate' | 'checkoutLineDelete' | 'checkoutLinesAdd' | 'checkoutLinesDelete' | 'checkoutLinesUpdate' | 'checkoutPaymentCreate' | 'checkoutRemovePromoCode' | 'checkoutShippingAddressUpdate' | 'checkoutShippingMethodUpdate' | 'collectionAddProducts' | 'collectionBulkDelete' | 'collectionChannelListingUpdate' | 'collectionCreate' | 'collectionDelete' | 'collectionRemoveProducts' | 'collectionReorderProducts' | 'collectionTranslate' | 'collectionUpdate' | 'confirmAccount' | 'confirmEmailChange' | 'createWarehouse' | 'customerBulkDelete' | 'customerBulkUpdate' | 'customerCreate' | 'customerDelete' | 'customerUpdate' | 'deleteMetadata' | 'deletePrivateMetadata' | 'deleteWarehouse' | 'digitalContentCreate' | 'digitalContentDelete' | 'digitalContentUpdate' | 'digitalContentUrlCreate' | 'draftOrderBulkDelete' | 'draftOrderComplete' | 'draftOrderCreate' | 'draftOrderDelete' | 'draftOrderLinesBulkDelete' | 'draftOrderUpdate' | 'eventDeliveryRetry' | 'exportGiftCards' | 'exportProducts' | 'externalAuthenticationUrl' | 'externalLogout' | 'externalNotificationTrigger' | 'externalObtainAccessTokens' | 'externalRefresh' | 'externalVerify' | 'fileUpload' | 'giftCardActivate' | 'giftCardAddNote' | 'giftCardBulkActivate' | 'giftCardBulkCreate' | 'giftCardBulkDeactivate' | 'giftCardBulkDelete' | 'giftCardCreate' | 'giftCardDeactivate' | 'giftCardDelete' | 'giftCardResend' | 'giftCardSettingsUpdate' | 'giftCardUpdate' | 'invoiceCreate' | 'invoiceDelete' | 'invoiceRequest' | 'invoiceRequestDelete' | 'invoiceSendNotification' | 'invoiceUpdate' | 'menuBulkDelete' | 'menuCreate' | 'menuDelete' | 'menuItemBulkDelete' | 'menuItemCreate' | 'menuItemDelete' | 'menuItemMove' | 'menuItemTranslate' | 'menuItemUpdate' | 'menuUpdate' | 'orderAddNote' | 'orderBulkCancel' | 'orderBulkCreate' | 'orderCancel' | 'orderCapture' | 'orderConfirm' | 'orderCreateFromCheckout' | 'orderDiscountAdd' | 'orderDiscountDelete' | 'orderDiscountUpdate' | 'orderFulfill' | 'orderFulfillmentApprove' | 'orderFulfillmentCancel' | 'orderFulfillmentRefundProducts' | 'orderFulfillmentReturnProducts' | 'orderFulfillmentUpdateTracking' | 'orderGrantRefundCreate' | 'orderGrantRefundUpdate' | 'orderLineDelete' | 'orderLineDiscountRemove' | 'orderLineDiscountUpdate' | 'orderLineUpdate' | 'orderLinesCreate' | 'orderMarkAsPaid' | 'orderNoteAdd' | 'orderNoteUpdate' | 'orderRefund' | 'orderSettingsUpdate' | 'orderUpdate' | 'orderUpdateShipping' | 'orderVoid' | 'pageAttributeAssign' | 'pageAttributeUnassign' | 'pageBulkDelete' | 'pageBulkPublish' | 'pageCreate' | 'pageDelete' | 'pageReorderAttributeValues' | 'pageTranslate' | 'pageTypeBulkDelete' | 'pageTypeCreate' | 'pageTypeDelete' | 'pageTypeReorderAttributes' | 'pageTypeUpdate' | 'pageUpdate' | 'passwordChange' | 'paymentCapture' | 'paymentCheckBalance' | 'paymentGatewayInitialize' | 'paymentGatewayInitializeTokenization' | 'paymentInitialize' | 'paymentMethodInitializeTokenization' | 'paymentMethodProcessTokenization' | 'paymentRefund' | 'paymentVoid' | 'permissionGroupCreate' | 'permissionGroupDelete' | 'permissionGroupUpdate' | 'pluginUpdate' | 'productAttributeAssign' | 'productAttributeAssignmentUpdate' | 'productAttributeUnassign' | 'productBulkCreate' | 'productBulkDelete' | 'productBulkTranslate' | 'productChannelListingUpdate' | 'productCreate' | 'productDelete' | 'productMediaBulkDelete' | 'productMediaCreate' | 'productMediaDelete' | 'productMediaReorder' | 'productMediaUpdate' | 'productReorderAttributeValues' | 'productTranslate' | 'productTypeBulkDelete' | 'productTypeCreate' | 'productTypeDelete' | 'productTypeReorderAttributes' | 'productTypeUpdate' | 'productUpdate' | 'productVariantBulkCreate' | 'productVariantBulkDelete' | 'productVariantBulkTranslate' | 'productVariantBulkUpdate' | 'productVariantChannelListingUpdate' | 'productVariantCreate' | 'productVariantDelete' | 'productVariantPreorderDeactivate' | 'productVariantReorder' | 'productVariantReorderAttributeValues' | 'productVariantSetDefault' | 'productVariantStocksCreate' | 'productVariantStocksDelete' | 'productVariantStocksUpdate' | 'productVariantTranslate' | 'productVariantUpdate' | 'promotionBulkDelete' | 'promotionCreate' | 'promotionDelete' | 'promotionRuleCreate' | 'promotionRuleDelete' | 'promotionRuleTranslate' | 'promotionRuleUpdate' | 'promotionTranslate' | 'promotionUpdate' | 'requestEmailChange' | 'requestPasswordReset' | 'saleBulkDelete' | 'saleCataloguesAdd' | 'saleCataloguesRemove' | 'saleChannelListingUpdate' | 'saleCreate' | 'saleDelete' | 'saleTranslate' | 'saleUpdate' | 'sendConfirmationEmail' | 'setPassword' | 'shippingMethodChannelListingUpdate' | 'shippingPriceBulkDelete' | 'shippingPriceCreate' | 'shippingPriceDelete' | 'shippingPriceExcludeProducts' | 'shippingPriceRemoveProductFromExclude' | 'shippingPriceTranslate' | 'shippingPriceUpdate' | 'shippingZoneBulkDelete' | 'shippingZoneCreate' | 'shippingZoneDelete' | 'shippingZoneUpdate' | 'shopAddressUpdate' | 'shopDomainUpdate' | 'shopFetchTaxRates' | 'shopSettingsTranslate' | 'shopSettingsUpdate' | 'staffBulkDelete' | 'staffCreate' | 'staffDelete' | 'staffNotificationRecipientCreate' | 'staffNotificationRecipientDelete' | 'staffNotificationRecipientUpdate' | 'staffUpdate' | 'stockBulkUpdate' | 'storedPaymentMethodRequestDelete' | 'taxClassCreate' | 'taxClassDelete' | 'taxClassUpdate' | 'taxConfigurationUpdate' | 'taxCountryConfigurationDelete' | 'taxCountryConfigurationUpdate' | 'taxExemptionManage' | 'tokenCreate' | 'tokenRefresh' | 'tokenVerify' | 'tokensDeactivateAll' | 'transactionCreate' | 'transactionEventReport' | 'transactionInitialize' | 'transactionProcess' | 'transactionRequestAction' | 'transactionRequestRefundForGrantedRefund' | 'transactionUpdate' | 'unassignWarehouseShippingZone' | 'updateMetadata' | 'updatePrivateMetadata' | 'updateWarehouse' | 'userAvatarDelete' | 'userAvatarUpdate' | 'userBulkSetActive' | 'variantMediaAssign' | 'variantMediaUnassign' | 'voucherBulkDelete' | 'voucherCataloguesAdd' | 'voucherCataloguesRemove' | 'voucherChannelListingUpdate' | 'voucherCreate' | 'voucherDelete' | 'voucherTranslate' | 'voucherUpdate' | 'webhookCreate' | 'webhookDelete' | 'webhookDryRun' | 'webhookTrigger' | 'webhookUpdate' | MutationKeySpecifier)[]; +export type MutationKeySpecifier = ('accountAddressCreate' | 'accountAddressDelete' | 'accountAddressUpdate' | 'accountDelete' | 'accountRegister' | 'accountRequestDeletion' | 'accountSetDefaultAddress' | 'accountUpdate' | 'addressCreate' | 'addressDelete' | 'addressSetDefault' | 'addressUpdate' | 'appActivate' | 'appCreate' | 'appDeactivate' | 'appDelete' | 'appDeleteFailedInstallation' | 'appFetchManifest' | 'appInstall' | 'appRetryInstall' | 'appTokenCreate' | 'appTokenDelete' | 'appTokenVerify' | 'appUpdate' | 'assignNavigation' | 'assignWarehouseShippingZone' | 'attributeBulkCreate' | 'attributeBulkDelete' | 'attributeBulkTranslate' | 'attributeBulkUpdate' | 'attributeCreate' | 'attributeDelete' | 'attributeReorderValues' | 'attributeTranslate' | 'attributeUpdate' | 'attributeValueBulkDelete' | 'attributeValueBulkTranslate' | 'attributeValueCreate' | 'attributeValueDelete' | 'attributeValueTranslate' | 'attributeValueUpdate' | 'categoryBulkDelete' | 'categoryCreate' | 'categoryDelete' | 'categoryTranslate' | 'categoryUpdate' | 'channelActivate' | 'channelCreate' | 'channelDeactivate' | 'channelDelete' | 'channelReorderWarehouses' | 'channelUpdate' | 'checkoutAddPromoCode' | 'checkoutBillingAddressUpdate' | 'checkoutComplete' | 'checkoutCreate' | 'checkoutCreateFromOrder' | 'checkoutCustomerAttach' | 'checkoutCustomerDetach' | 'checkoutDeliveryMethodUpdate' | 'checkoutEmailUpdate' | 'checkoutLanguageCodeUpdate' | 'checkoutLineDelete' | 'checkoutLinesAdd' | 'checkoutLinesDelete' | 'checkoutLinesUpdate' | 'checkoutPaymentCreate' | 'checkoutRemovePromoCode' | 'checkoutShippingAddressUpdate' | 'checkoutShippingMethodUpdate' | 'collectionAddProducts' | 'collectionBulkDelete' | 'collectionChannelListingUpdate' | 'collectionCreate' | 'collectionDelete' | 'collectionRemoveProducts' | 'collectionReorderProducts' | 'collectionTranslate' | 'collectionUpdate' | 'confirmAccount' | 'confirmEmailChange' | 'createWarehouse' | 'customerBulkDelete' | 'customerBulkUpdate' | 'customerCreate' | 'customerDelete' | 'customerUpdate' | 'deleteMetadata' | 'deletePrivateMetadata' | 'deleteWarehouse' | 'digitalContentCreate' | 'digitalContentDelete' | 'digitalContentUpdate' | 'digitalContentUrlCreate' | 'draftOrderBulkDelete' | 'draftOrderComplete' | 'draftOrderCreate' | 'draftOrderDelete' | 'draftOrderLinesBulkDelete' | 'draftOrderUpdate' | 'eventDeliveryRetry' | 'exportGiftCards' | 'exportProducts' | 'exportVoucherCodes' | 'externalAuthenticationUrl' | 'externalLogout' | 'externalNotificationTrigger' | 'externalObtainAccessTokens' | 'externalRefresh' | 'externalVerify' | 'fileUpload' | 'giftCardActivate' | 'giftCardAddNote' | 'giftCardBulkActivate' | 'giftCardBulkCreate' | 'giftCardBulkDeactivate' | 'giftCardBulkDelete' | 'giftCardCreate' | 'giftCardDeactivate' | 'giftCardDelete' | 'giftCardResend' | 'giftCardSettingsUpdate' | 'giftCardUpdate' | 'invoiceCreate' | 'invoiceDelete' | 'invoiceRequest' | 'invoiceRequestDelete' | 'invoiceSendNotification' | 'invoiceUpdate' | 'menuBulkDelete' | 'menuCreate' | 'menuDelete' | 'menuItemBulkDelete' | 'menuItemCreate' | 'menuItemDelete' | 'menuItemMove' | 'menuItemTranslate' | 'menuItemUpdate' | 'menuUpdate' | 'orderAddNote' | 'orderBulkCancel' | 'orderBulkCreate' | 'orderCancel' | 'orderCapture' | 'orderConfirm' | 'orderCreateFromCheckout' | 'orderDiscountAdd' | 'orderDiscountDelete' | 'orderDiscountUpdate' | 'orderFulfill' | 'orderFulfillmentApprove' | 'orderFulfillmentCancel' | 'orderFulfillmentRefundProducts' | 'orderFulfillmentReturnProducts' | 'orderFulfillmentUpdateTracking' | 'orderGrantRefundCreate' | 'orderGrantRefundUpdate' | 'orderLineDelete' | 'orderLineDiscountRemove' | 'orderLineDiscountUpdate' | 'orderLineUpdate' | 'orderLinesCreate' | 'orderMarkAsPaid' | 'orderNoteAdd' | 'orderNoteUpdate' | 'orderRefund' | 'orderSettingsUpdate' | 'orderUpdate' | 'orderUpdateShipping' | 'orderVoid' | 'pageAttributeAssign' | 'pageAttributeUnassign' | 'pageBulkDelete' | 'pageBulkPublish' | 'pageCreate' | 'pageDelete' | 'pageReorderAttributeValues' | 'pageTranslate' | 'pageTypeBulkDelete' | 'pageTypeCreate' | 'pageTypeDelete' | 'pageTypeReorderAttributes' | 'pageTypeUpdate' | 'pageUpdate' | 'passwordChange' | 'paymentCapture' | 'paymentCheckBalance' | 'paymentGatewayInitialize' | 'paymentGatewayInitializeTokenization' | 'paymentInitialize' | 'paymentMethodInitializeTokenization' | 'paymentMethodProcessTokenization' | 'paymentRefund' | 'paymentVoid' | 'permissionGroupCreate' | 'permissionGroupDelete' | 'permissionGroupUpdate' | 'pluginUpdate' | 'productAttributeAssign' | 'productAttributeAssignmentUpdate' | 'productAttributeUnassign' | 'productBulkCreate' | 'productBulkDelete' | 'productBulkTranslate' | 'productChannelListingUpdate' | 'productCreate' | 'productDelete' | 'productMediaBulkDelete' | 'productMediaCreate' | 'productMediaDelete' | 'productMediaReorder' | 'productMediaUpdate' | 'productReorderAttributeValues' | 'productTranslate' | 'productTypeBulkDelete' | 'productTypeCreate' | 'productTypeDelete' | 'productTypeReorderAttributes' | 'productTypeUpdate' | 'productUpdate' | 'productVariantBulkCreate' | 'productVariantBulkDelete' | 'productVariantBulkTranslate' | 'productVariantBulkUpdate' | 'productVariantChannelListingUpdate' | 'productVariantCreate' | 'productVariantDelete' | 'productVariantPreorderDeactivate' | 'productVariantReorder' | 'productVariantReorderAttributeValues' | 'productVariantSetDefault' | 'productVariantStocksCreate' | 'productVariantStocksDelete' | 'productVariantStocksUpdate' | 'productVariantTranslate' | 'productVariantUpdate' | 'promotionBulkDelete' | 'promotionCreate' | 'promotionDelete' | 'promotionRuleCreate' | 'promotionRuleDelete' | 'promotionRuleTranslate' | 'promotionRuleUpdate' | 'promotionTranslate' | 'promotionUpdate' | 'requestEmailChange' | 'requestPasswordReset' | 'saleBulkDelete' | 'saleCataloguesAdd' | 'saleCataloguesRemove' | 'saleChannelListingUpdate' | 'saleCreate' | 'saleDelete' | 'saleTranslate' | 'saleUpdate' | 'sendConfirmationEmail' | 'setPassword' | 'shippingMethodChannelListingUpdate' | 'shippingPriceBulkDelete' | 'shippingPriceCreate' | 'shippingPriceDelete' | 'shippingPriceExcludeProducts' | 'shippingPriceRemoveProductFromExclude' | 'shippingPriceTranslate' | 'shippingPriceUpdate' | 'shippingZoneBulkDelete' | 'shippingZoneCreate' | 'shippingZoneDelete' | 'shippingZoneUpdate' | 'shopAddressUpdate' | 'shopDomainUpdate' | 'shopFetchTaxRates' | 'shopSettingsTranslate' | 'shopSettingsUpdate' | 'staffBulkDelete' | 'staffCreate' | 'staffDelete' | 'staffNotificationRecipientCreate' | 'staffNotificationRecipientDelete' | 'staffNotificationRecipientUpdate' | 'staffUpdate' | 'stockBulkUpdate' | 'storedPaymentMethodRequestDelete' | 'taxClassCreate' | 'taxClassDelete' | 'taxClassUpdate' | 'taxConfigurationUpdate' | 'taxCountryConfigurationDelete' | 'taxCountryConfigurationUpdate' | 'taxExemptionManage' | 'tokenCreate' | 'tokenRefresh' | 'tokenVerify' | 'tokensDeactivateAll' | 'transactionCreate' | 'transactionEventReport' | 'transactionInitialize' | 'transactionProcess' | 'transactionRequestAction' | 'transactionRequestRefundForGrantedRefund' | 'transactionUpdate' | 'unassignWarehouseShippingZone' | 'updateMetadata' | 'updatePrivateMetadata' | 'updateWarehouse' | 'userAvatarDelete' | 'userAvatarUpdate' | 'userBulkSetActive' | 'variantMediaAssign' | 'variantMediaUnassign' | 'voucherBulkDelete' | 'voucherCataloguesAdd' | 'voucherCataloguesRemove' | 'voucherChannelListingUpdate' | 'voucherCodeBulkDelete' | 'voucherCreate' | 'voucherDelete' | 'voucherTranslate' | 'voucherUpdate' | 'webhookCreate' | 'webhookDelete' | 'webhookDryRun' | 'webhookTrigger' | 'webhookUpdate' | MutationKeySpecifier)[]; export type MutationFieldPolicy = { accountAddressCreate?: FieldPolicy | FieldReadFunction, accountAddressDelete?: FieldPolicy | FieldReadFunction, @@ -2721,6 +2730,7 @@ export type MutationFieldPolicy = { eventDeliveryRetry?: FieldPolicy | FieldReadFunction, exportGiftCards?: FieldPolicy | FieldReadFunction, exportProducts?: FieldPolicy | FieldReadFunction, + exportVoucherCodes?: FieldPolicy | FieldReadFunction, externalAuthenticationUrl?: FieldPolicy | FieldReadFunction, externalLogout?: FieldPolicy | FieldReadFunction, externalNotificationTrigger?: FieldPolicy | FieldReadFunction, @@ -2931,6 +2941,7 @@ export type MutationFieldPolicy = { voucherCataloguesAdd?: FieldPolicy | FieldReadFunction, voucherCataloguesRemove?: FieldPolicy | FieldReadFunction, voucherChannelListingUpdate?: FieldPolicy | FieldReadFunction, + voucherCodeBulkDelete?: FieldPolicy | FieldReadFunction, voucherCreate?: FieldPolicy | FieldReadFunction, voucherDelete?: FieldPolicy | FieldReadFunction, voucherTranslate?: FieldPolicy | FieldReadFunction, @@ -2954,7 +2965,7 @@ export type ObjectWithMetadataFieldPolicy = { privateMetafield?: FieldPolicy | FieldReadFunction, privateMetafields?: FieldPolicy | FieldReadFunction }; -export type OrderKeySpecifier = ('actions' | 'authorizeStatus' | 'availableCollectionPoints' | 'availableShippingMethods' | 'billingAddress' | 'canFinalize' | 'channel' | 'chargeStatus' | 'checkoutId' | 'collectionPointName' | 'created' | 'customerNote' | 'deliveryMethod' | 'discount' | 'discountName' | 'discounts' | 'displayGrossPrices' | 'errors' | 'events' | 'externalReference' | 'fulfillments' | 'giftCards' | 'grantedRefunds' | 'id' | 'invoices' | 'isPaid' | 'isShippingRequired' | 'languageCode' | 'languageCodeEnum' | 'lines' | 'metadata' | 'metafield' | 'metafields' | 'number' | 'origin' | 'original' | 'paymentStatus' | 'paymentStatusDisplay' | 'payments' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'redirectUrl' | 'shippingAddress' | 'shippingMethod' | 'shippingMethodName' | 'shippingMethods' | 'shippingPrice' | 'shippingTaxClass' | 'shippingTaxClassMetadata' | 'shippingTaxClassName' | 'shippingTaxClassPrivateMetadata' | 'shippingTaxRate' | 'status' | 'statusDisplay' | 'subtotal' | 'taxExemption' | 'token' | 'total' | 'totalAuthorizePending' | 'totalAuthorized' | 'totalBalance' | 'totalCancelPending' | 'totalCanceled' | 'totalCaptured' | 'totalChargePending' | 'totalCharged' | 'totalGrantedRefund' | 'totalRefundPending' | 'totalRefunded' | 'totalRemainingGrant' | 'trackingClientId' | 'transactions' | 'translatedDiscountName' | 'undiscountedTotal' | 'updatedAt' | 'user' | 'userEmail' | 'voucher' | 'weight' | OrderKeySpecifier)[]; +export type OrderKeySpecifier = ('actions' | 'authorizeStatus' | 'availableCollectionPoints' | 'availableShippingMethods' | 'billingAddress' | 'canFinalize' | 'channel' | 'chargeStatus' | 'checkoutId' | 'collectionPointName' | 'created' | 'customerNote' | 'deliveryMethod' | 'discount' | 'discountName' | 'discounts' | 'displayGrossPrices' | 'errors' | 'events' | 'externalReference' | 'fulfillments' | 'giftCards' | 'grantedRefunds' | 'id' | 'invoices' | 'isPaid' | 'isShippingRequired' | 'languageCode' | 'languageCodeEnum' | 'lines' | 'metadata' | 'metafield' | 'metafields' | 'number' | 'origin' | 'original' | 'paymentStatus' | 'paymentStatusDisplay' | 'payments' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'redirectUrl' | 'shippingAddress' | 'shippingMethod' | 'shippingMethodName' | 'shippingMethods' | 'shippingPrice' | 'shippingTaxClass' | 'shippingTaxClassMetadata' | 'shippingTaxClassName' | 'shippingTaxClassPrivateMetadata' | 'shippingTaxRate' | 'status' | 'statusDisplay' | 'subtotal' | 'taxExemption' | 'token' | 'total' | 'totalAuthorizePending' | 'totalAuthorized' | 'totalBalance' | 'totalCancelPending' | 'totalCanceled' | 'totalCaptured' | 'totalChargePending' | 'totalCharged' | 'totalGrantedRefund' | 'totalRefundPending' | 'totalRefunded' | 'totalRemainingGrant' | 'trackingClientId' | 'transactions' | 'translatedDiscountName' | 'undiscountedTotal' | 'updatedAt' | 'user' | 'userEmail' | 'voucher' | 'voucherCode' | 'weight' | OrderKeySpecifier)[]; export type OrderFieldPolicy = { actions?: FieldPolicy | FieldReadFunction, authorizeStatus?: FieldPolicy | FieldReadFunction, @@ -3035,6 +3046,7 @@ export type OrderFieldPolicy = { user?: FieldPolicy | FieldReadFunction, userEmail?: FieldPolicy | FieldReadFunction, voucher?: FieldPolicy | FieldReadFunction, + voucherCode?: FieldPolicy | FieldReadFunction, weight?: FieldPolicy | FieldReadFunction }; export type OrderAddNoteKeySpecifier = ('errors' | 'event' | 'order' | 'orderErrors' | OrderAddNoteKeySpecifier)[]; @@ -3344,7 +3356,7 @@ export type OrderGrantedRefundLineFieldPolicy = { quantity?: FieldPolicy | FieldReadFunction, reason?: FieldPolicy | FieldReadFunction }; -export type OrderLineKeySpecifier = ('allocations' | 'digitalContentUrl' | 'id' | 'isShippingRequired' | 'metadata' | 'metafield' | 'metafields' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'productName' | 'productSku' | 'productVariantId' | 'quantity' | 'quantityFulfilled' | 'quantityToFulfill' | 'taxClass' | 'taxClassMetadata' | 'taxClassName' | 'taxClassPrivateMetadata' | 'taxRate' | 'thumbnail' | 'totalPrice' | 'translatedProductName' | 'translatedVariantName' | 'undiscountedTotalPrice' | 'undiscountedUnitPrice' | 'unitDiscount' | 'unitDiscountReason' | 'unitDiscountType' | 'unitDiscountValue' | 'unitPrice' | 'variant' | 'variantName' | OrderLineKeySpecifier)[]; +export type OrderLineKeySpecifier = ('allocations' | 'digitalContentUrl' | 'id' | 'isShippingRequired' | 'metadata' | 'metafield' | 'metafields' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'productName' | 'productSku' | 'productVariantId' | 'quantity' | 'quantityFulfilled' | 'quantityToFulfill' | 'saleId' | 'taxClass' | 'taxClassMetadata' | 'taxClassName' | 'taxClassPrivateMetadata' | 'taxRate' | 'thumbnail' | 'totalPrice' | 'translatedProductName' | 'translatedVariantName' | 'undiscountedTotalPrice' | 'undiscountedUnitPrice' | 'unitDiscount' | 'unitDiscountReason' | 'unitDiscountType' | 'unitDiscountValue' | 'unitPrice' | 'variant' | 'variantName' | 'voucherCode' | OrderLineKeySpecifier)[]; export type OrderLineFieldPolicy = { allocations?: FieldPolicy | FieldReadFunction, digitalContentUrl?: FieldPolicy | FieldReadFunction, @@ -3362,6 +3374,7 @@ export type OrderLineFieldPolicy = { quantity?: FieldPolicy | FieldReadFunction, quantityFulfilled?: FieldPolicy | FieldReadFunction, quantityToFulfill?: FieldPolicy | FieldReadFunction, + saleId?: FieldPolicy | FieldReadFunction, taxClass?: FieldPolicy | FieldReadFunction, taxClassMetadata?: FieldPolicy | FieldReadFunction, taxClassName?: FieldPolicy | FieldReadFunction, @@ -3379,7 +3392,8 @@ export type OrderLineFieldPolicy = { unitDiscountValue?: FieldPolicy | FieldReadFunction, unitPrice?: FieldPolicy | FieldReadFunction, variant?: FieldPolicy | FieldReadFunction, - variantName?: FieldPolicy | FieldReadFunction + variantName?: FieldPolicy | FieldReadFunction, + voucherCode?: FieldPolicy | FieldReadFunction }; export type OrderLineDeleteKeySpecifier = ('errors' | 'order' | 'orderErrors' | 'orderLine' | OrderLineDeleteKeySpecifier)[]; export type OrderLineDeleteFieldPolicy = { @@ -3476,14 +3490,14 @@ export type OrderRefundedFieldPolicy = { recipient?: FieldPolicy | FieldReadFunction, version?: FieldPolicy | FieldReadFunction }; -export type OrderSettingsKeySpecifier = ('allowUnpaidOrders' | 'automaticallyConfirmAllNewOrders' | 'automaticallyFulfillNonShippableGiftCard' | 'defaultTransactionFlowStrategy' | 'deleteExpiredOrdersAfter' | 'expireOrdersAfter' | 'markAsPaidStrategy' | OrderSettingsKeySpecifier)[]; +export type OrderSettingsKeySpecifier = ('allowUnpaidOrders' | 'automaticallyConfirmAllNewOrders' | 'automaticallyFulfillNonShippableGiftCard' | 'deleteExpiredOrdersAfter' | 'expireOrdersAfter' | 'includeDraftOrderInVoucherUsage' | 'markAsPaidStrategy' | OrderSettingsKeySpecifier)[]; export type OrderSettingsFieldPolicy = { allowUnpaidOrders?: FieldPolicy | FieldReadFunction, automaticallyConfirmAllNewOrders?: FieldPolicy | FieldReadFunction, automaticallyFulfillNonShippableGiftCard?: FieldPolicy | FieldReadFunction, - defaultTransactionFlowStrategy?: FieldPolicy | FieldReadFunction, deleteExpiredOrdersAfter?: FieldPolicy | FieldReadFunction, expireOrdersAfter?: FieldPolicy | FieldReadFunction, + includeDraftOrderInVoucherUsage?: FieldPolicy | FieldReadFunction, markAsPaidStrategy?: FieldPolicy | FieldReadFunction }; export type OrderSettingsErrorKeySpecifier = ('code' | 'field' | 'message' | OrderSettingsErrorKeySpecifier)[]; @@ -3759,7 +3773,7 @@ export type PasswordChangeFieldPolicy = { errors?: FieldPolicy | FieldReadFunction, user?: FieldPolicy | FieldReadFunction }; -export type PaymentKeySpecifier = ('actions' | 'availableCaptureAmount' | 'availableRefundAmount' | 'capturedAmount' | 'chargeStatus' | 'checkout' | 'created' | 'creditCard' | 'customerIpAddress' | 'gateway' | 'id' | 'isActive' | 'metadata' | 'metafield' | 'metafields' | 'modified' | 'order' | 'paymentMethodType' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'token' | 'total' | 'transactions' | PaymentKeySpecifier)[]; +export type PaymentKeySpecifier = ('actions' | 'availableCaptureAmount' | 'availableRefundAmount' | 'capturedAmount' | 'chargeStatus' | 'checkout' | 'created' | 'creditCard' | 'customerIpAddress' | 'gateway' | 'id' | 'isActive' | 'metadata' | 'metafield' | 'metafields' | 'modified' | 'order' | 'partial' | 'paymentMethodType' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'pspReference' | 'token' | 'total' | 'transactions' | PaymentKeySpecifier)[]; export type PaymentFieldPolicy = { actions?: FieldPolicy | FieldReadFunction, availableCaptureAmount?: FieldPolicy | FieldReadFunction, @@ -3778,10 +3792,12 @@ export type PaymentFieldPolicy = { metafields?: FieldPolicy | FieldReadFunction, modified?: FieldPolicy | FieldReadFunction, order?: FieldPolicy | FieldReadFunction, + partial?: FieldPolicy | FieldReadFunction, paymentMethodType?: FieldPolicy | FieldReadFunction, privateMetadata?: FieldPolicy | FieldReadFunction, privateMetafield?: FieldPolicy | FieldReadFunction, privateMetafields?: FieldPolicy | FieldReadFunction, + pspReference?: FieldPolicy | FieldReadFunction, token?: FieldPolicy | FieldReadFunction, total?: FieldPolicy | FieldReadFunction, transactions?: FieldPolicy | FieldReadFunction @@ -6453,13 +6469,14 @@ export type VerifyTokenFieldPolicy = { payload?: FieldPolicy | FieldReadFunction, user?: FieldPolicy | FieldReadFunction }; -export type VoucherKeySpecifier = ('applyOncePerCustomer' | 'applyOncePerOrder' | 'categories' | 'channelListings' | 'code' | 'collections' | 'countries' | 'currency' | 'discountValue' | 'discountValueType' | 'endDate' | 'id' | 'metadata' | 'metafield' | 'metafields' | 'minCheckoutItemsQuantity' | 'minSpent' | 'name' | 'onlyForStaff' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'products' | 'startDate' | 'translation' | 'type' | 'usageLimit' | 'used' | 'variants' | VoucherKeySpecifier)[]; +export type VoucherKeySpecifier = ('applyOncePerCustomer' | 'applyOncePerOrder' | 'categories' | 'channelListings' | 'code' | 'codes' | 'collections' | 'countries' | 'currency' | 'discountValue' | 'discountValueType' | 'endDate' | 'id' | 'metadata' | 'metafield' | 'metafields' | 'minCheckoutItemsQuantity' | 'minSpent' | 'name' | 'onlyForStaff' | 'privateMetadata' | 'privateMetafield' | 'privateMetafields' | 'products' | 'singleUse' | 'startDate' | 'translation' | 'type' | 'usageLimit' | 'used' | 'variants' | VoucherKeySpecifier)[]; export type VoucherFieldPolicy = { applyOncePerCustomer?: FieldPolicy | FieldReadFunction, applyOncePerOrder?: FieldPolicy | FieldReadFunction, categories?: FieldPolicy | FieldReadFunction, channelListings?: FieldPolicy | FieldReadFunction, code?: FieldPolicy | FieldReadFunction, + codes?: FieldPolicy | FieldReadFunction, collections?: FieldPolicy | FieldReadFunction, countries?: FieldPolicy | FieldReadFunction, currency?: FieldPolicy | FieldReadFunction, @@ -6478,6 +6495,7 @@ export type VoucherFieldPolicy = { privateMetafield?: FieldPolicy | FieldReadFunction, privateMetafields?: FieldPolicy | FieldReadFunction, products?: FieldPolicy | FieldReadFunction, + singleUse?: FieldPolicy | FieldReadFunction, startDate?: FieldPolicy | FieldReadFunction, translation?: FieldPolicy | FieldReadFunction, type?: FieldPolicy | FieldReadFunction, @@ -6511,6 +6529,45 @@ export type VoucherChannelListingUpdateFieldPolicy = { errors?: FieldPolicy | FieldReadFunction, voucher?: FieldPolicy | FieldReadFunction }; +export type VoucherCodeKeySpecifier = ('code' | 'createdAt' | 'id' | 'isActive' | 'used' | VoucherCodeKeySpecifier)[]; +export type VoucherCodeFieldPolicy = { + code?: FieldPolicy | FieldReadFunction, + createdAt?: FieldPolicy | FieldReadFunction, + id?: FieldPolicy | FieldReadFunction, + isActive?: FieldPolicy | FieldReadFunction, + used?: FieldPolicy | FieldReadFunction +}; +export type VoucherCodeBulkDeleteKeySpecifier = ('count' | 'errors' | VoucherCodeBulkDeleteKeySpecifier)[]; +export type VoucherCodeBulkDeleteFieldPolicy = { + count?: FieldPolicy | FieldReadFunction, + errors?: FieldPolicy | FieldReadFunction +}; +export type VoucherCodeBulkDeleteErrorKeySpecifier = ('code' | 'message' | 'path' | 'voucherCodes' | VoucherCodeBulkDeleteErrorKeySpecifier)[]; +export type VoucherCodeBulkDeleteErrorFieldPolicy = { + code?: FieldPolicy | FieldReadFunction, + message?: FieldPolicy | FieldReadFunction, + path?: FieldPolicy | FieldReadFunction, + voucherCodes?: FieldPolicy | FieldReadFunction +}; +export type VoucherCodeCountableConnectionKeySpecifier = ('edges' | 'pageInfo' | 'totalCount' | VoucherCodeCountableConnectionKeySpecifier)[]; +export type VoucherCodeCountableConnectionFieldPolicy = { + edges?: FieldPolicy | FieldReadFunction, + pageInfo?: FieldPolicy | FieldReadFunction, + totalCount?: FieldPolicy | FieldReadFunction +}; +export type VoucherCodeCountableEdgeKeySpecifier = ('cursor' | 'node' | VoucherCodeCountableEdgeKeySpecifier)[]; +export type VoucherCodeCountableEdgeFieldPolicy = { + cursor?: FieldPolicy | FieldReadFunction, + node?: FieldPolicy | FieldReadFunction +}; +export type VoucherCodeExportCompletedKeySpecifier = ('export' | 'issuedAt' | 'issuingPrincipal' | 'recipient' | 'version' | VoucherCodeExportCompletedKeySpecifier)[]; +export type VoucherCodeExportCompletedFieldPolicy = { + export?: FieldPolicy | FieldReadFunction, + issuedAt?: FieldPolicy | FieldReadFunction, + issuingPrincipal?: FieldPolicy | FieldReadFunction, + recipient?: FieldPolicy | FieldReadFunction, + version?: FieldPolicy | FieldReadFunction +}; export type VoucherCountableConnectionKeySpecifier = ('edges' | 'pageInfo' | 'totalCount' | VoucherCountableConnectionKeySpecifier)[]; export type VoucherCountableConnectionFieldPolicy = { edges?: FieldPolicy | FieldReadFunction, @@ -7742,6 +7799,10 @@ export type StrictTypedTypePolicies = { keyFields?: false | ExportProductsKeySpecifier | (() => undefined | ExportProductsKeySpecifier), fields?: ExportProductsFieldPolicy, }, + ExportVoucherCodes?: Omit & { + keyFields?: false | ExportVoucherCodesKeySpecifier | (() => undefined | ExportVoucherCodesKeySpecifier), + fields?: ExportVoucherCodesFieldPolicy, + }, ExternalAuthentication?: Omit & { keyFields?: false | ExternalAuthenticationKeySpecifier | (() => undefined | ExternalAuthenticationKeySpecifier), fields?: ExternalAuthenticationFieldPolicy, @@ -9926,6 +9987,30 @@ export type StrictTypedTypePolicies = { keyFields?: false | VoucherChannelListingUpdateKeySpecifier | (() => undefined | VoucherChannelListingUpdateKeySpecifier), fields?: VoucherChannelListingUpdateFieldPolicy, }, + VoucherCode?: Omit & { + keyFields?: false | VoucherCodeKeySpecifier | (() => undefined | VoucherCodeKeySpecifier), + fields?: VoucherCodeFieldPolicy, + }, + VoucherCodeBulkDelete?: Omit & { + keyFields?: false | VoucherCodeBulkDeleteKeySpecifier | (() => undefined | VoucherCodeBulkDeleteKeySpecifier), + fields?: VoucherCodeBulkDeleteFieldPolicy, + }, + VoucherCodeBulkDeleteError?: Omit & { + keyFields?: false | VoucherCodeBulkDeleteErrorKeySpecifier | (() => undefined | VoucherCodeBulkDeleteErrorKeySpecifier), + fields?: VoucherCodeBulkDeleteErrorFieldPolicy, + }, + VoucherCodeCountableConnection?: Omit & { + keyFields?: false | VoucherCodeCountableConnectionKeySpecifier | (() => undefined | VoucherCodeCountableConnectionKeySpecifier), + fields?: VoucherCodeCountableConnectionFieldPolicy, + }, + VoucherCodeCountableEdge?: Omit & { + keyFields?: false | VoucherCodeCountableEdgeKeySpecifier | (() => undefined | VoucherCodeCountableEdgeKeySpecifier), + fields?: VoucherCodeCountableEdgeFieldPolicy, + }, + VoucherCodeExportCompleted?: Omit & { + keyFields?: false | VoucherCodeExportCompletedKeySpecifier | (() => undefined | VoucherCodeExportCompletedKeySpecifier), + fields?: VoucherCodeExportCompletedFieldPolicy, + }, VoucherCountableConnection?: Omit & { keyFields?: false | VoucherCountableConnectionKeySpecifier | (() => undefined | VoucherCountableConnectionKeySpecifier), fields?: VoucherCountableConnectionFieldPolicy, diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts index 62b608f2fb2..24ba02e3947 100644 --- a/src/graphql/types.generated.ts +++ b/src/graphql/types.generated.ts @@ -2025,7 +2025,8 @@ export enum DiscountErrorCode { INVALID = 'INVALID', NOT_FOUND = 'NOT_FOUND', REQUIRED = 'REQUIRED', - UNIQUE = 'UNIQUE' + UNIQUE = 'UNIQUE', + VOUCHER_ALREADY_USED = 'VOUCHER_ALREADY_USED' } export enum DiscountStatusEnum { @@ -2080,6 +2081,12 @@ export type DraftOrderCreateInput = { userEmail?: InputMaybe; /** ID of the voucher associated with the order. */ voucher?: InputMaybe; + /** + * A code of the voucher associated with the order. + * + * Added in Saleor 3.18. + */ + voucherCode?: InputMaybe; }; export type DraftOrderInput = { @@ -2109,6 +2116,12 @@ export type DraftOrderInput = { userEmail?: InputMaybe; /** ID of the voucher associated with the order. */ voucher?: InputMaybe; + /** + * A code of the voucher associated with the order. + * + * Added in Saleor 3.18. + */ + voucherCode?: InputMaybe; }; export enum ErrorPolicyEnum { @@ -2239,6 +2252,15 @@ export enum ExportScope { IDS = 'IDS' } +export type ExportVoucherCodesInput = { + /** Type of exported file. */ + fileType: FileTypesEnum; + /** List of voucher code IDs to export. */ + ids?: InputMaybe>; + /** The ID of the voucher. If provided, exports all codes belonging to the voucher. */ + voucherId?: InputMaybe; +}; + /** An enumeration. */ export enum ExternalNotificationErrorCodes { CHANNEL_INACTIVE = 'CHANNEL_INACTIVE', @@ -3709,7 +3731,7 @@ export type OrderBulkCreateInput = { deliveryMethod?: InputMaybe; /** List of discounts. */ discounts?: InputMaybe>; - /** Determines whether checkout prices should include taxes, when displayed in a storefront. */ + /** Determines whether displayed prices should include taxes. */ displayGrossPrices?: InputMaybe; /** External ID of the order. */ externalReference?: InputMaybe; @@ -3739,8 +3761,18 @@ export type OrderBulkCreateInput = { transactions?: InputMaybe>; /** Customer associated with the order. */ user: OrderBulkCreateUserInput; - /** Code of a voucher associated with the order. */ + /** + * Code of a voucher associated with the order. + * + * DEPRECATED: this field will be removed in Saleor 3.19. Use `voucherCode` instead. + */ voucher?: InputMaybe; + /** + * Code of a voucher associated with the order. + * + * Added in Saleor 3.18. + */ + voucherCode?: InputMaybe; /** Weight of the order in kg. */ weight?: InputMaybe; }; @@ -3923,6 +3955,8 @@ export enum OrderErrorCode { INSUFFICIENT_STOCK = 'INSUFFICIENT_STOCK', INVALID = 'INVALID', INVALID_QUANTITY = 'INVALID_QUANTITY', + INVALID_VOUCHER = 'INVALID_VOUCHER', + INVALID_VOUCHER_CODE = 'INVALID_VOUCHER_CODE', NOT_AVAILABLE_IN_CHANNEL = 'NOT_AVAILABLE_IN_CHANNEL', NOT_EDITABLE = 'NOT_EDITABLE', NOT_FOUND = 'NOT_FOUND', @@ -4286,16 +4320,6 @@ export type OrderSettingsInput = { automaticallyConfirmAllNewOrders?: InputMaybe; /** When enabled, all non-shippable gift card orders will be fulfilled automatically. By defualt set to True. */ automaticallyFulfillNonShippableGiftCard?: InputMaybe; - /** - * Determine the transaction flow strategy to be used. Include the selected option in the payload sent to the payment app, as a requested action for the transaction. - * - * Added in Saleor 3.13. - * - * Note: this API is currently in Feature Preview and can be subject to changes at later point. - * - * DEPRECATED: this preview feature field will be removed in Saleor 3.17. Use `PaymentSettingsInput.defaultTransactionFlowStrategy` instead. - */ - defaultTransactionFlowStrategy?: InputMaybe; /** * The time in days after expired orders will be deleted.Allowed range is from 1 to 120. * @@ -4312,6 +4336,16 @@ export type OrderSettingsInput = { * Note: this API is currently in Feature Preview and can be subject to changes at later point. */ expireOrdersAfter?: InputMaybe; + /** + * Specify whether a coupon applied to draft orders will count toward voucher usage. + * + * Warning: when switching this setting from `false` to `true`, the vouchers will be disconnected from all draft orders. + * + * Added in Saleor 3.18. + * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + */ + includeDraftOrderInVoucherUsage?: InputMaybe; /** * Determine what strategy will be used to mark the order as paid. Based on the chosen option, the proper object will be created and attached to the order when it's manually marked as paid. * `PAYMENT_FLOW` - [default option] creates the `Payment` object. @@ -4327,7 +4361,7 @@ export type OrderSettingsInput = { export type OrderSettingsUpdateInput = { /** When disabled, all new orders from checkout will be marked as unconfirmed. When enabled orders from checkout will become unfulfilled immediately. By default set to True */ automaticallyConfirmAllNewOrders?: InputMaybe; - /** When enabled, all non-shippable gift card orders will be fulfilled automatically. By defualt set to True. */ + /** When enabled, all non-shippable gift card orders will be fulfilled automatically. By default set to True. */ automaticallyFulfillNonShippableGiftCard?: InputMaybe; }; @@ -6706,7 +6740,7 @@ export type TaxConfigurationPerCountryInput = { chargeTaxes: Scalars['Boolean']; /** Country in which this configuration applies. */ countryCode: CountryCode; - /** Determines whether prices displayed in a storefront should include taxes for this country. */ + /** Determines whether displayed prices should include taxes for this country. */ displayGrossPrices: Scalars['Boolean']; /** A country-specific strategy to use for tax calculation. Taxes can be calculated either using user-defined flat rates or with a tax app. If not provided, use the value from the channel's tax configuration. */ taxCalculationStrategy?: InputMaybe; @@ -6723,7 +6757,7 @@ export enum TaxConfigurationUpdateErrorCode { export type TaxConfigurationUpdateInput = { /** Determines whether taxes are charged in the given channel. */ chargeTaxes?: InputMaybe; - /** Determines whether prices displayed in a storefront should include taxes. */ + /** Determines whether displayed prices should include taxes. */ displayGrossPrices?: InputMaybe; /** Determines whether prices are entered with the tax included. */ pricesEnteredWithTax?: InputMaybe; @@ -7225,6 +7259,13 @@ export type VoucherChannelListingInput = { removeChannels?: InputMaybe>; }; +/** An enumeration. */ +export enum VoucherCodeBulkDeleteErrorCode { + GRAPHQL_ERROR = 'GRAPHQL_ERROR', + INVALID = 'INVALID', + NOT_FOUND = 'NOT_FOUND' +} + export enum VoucherDiscountType { FIXED = 'FIXED', PERCENTAGE = 'PERCENTAGE', @@ -7242,13 +7283,21 @@ export type VoucherFilterInput = { }; export type VoucherInput = { + /** + * List of codes to add. + * + * Added in Saleor 3.18. + * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + */ + addCodes?: InputMaybe>; /** Voucher should be applied once per customer. */ applyOncePerCustomer?: InputMaybe; /** Voucher should be applied to the cheapest item or entire order. */ applyOncePerOrder?: InputMaybe; /** Categories discounted by the voucher. */ categories?: InputMaybe>; - /** Code to use the voucher. */ + /** Code to use the voucher. This field will be removed in Saleor 4.0. Use `addCodes` instead. */ code?: InputMaybe; /** Collections discounted by the voucher. */ collections?: InputMaybe>; @@ -7266,6 +7315,16 @@ export type VoucherInput = { onlyForStaff?: InputMaybe; /** Products discounted by the voucher. */ products?: InputMaybe>; + /** + * When set to 'True', each voucher code can be used only once; otherwise, codes can be used multiple times depending on `usageLimit`. + * + * The option can only be changed if none of the voucher codes have been used. + * + * Added in Saleor 3.18. + * + * Note: this API is currently in Feature Preview and can be subject to changes at later point. + */ + singleUse?: InputMaybe; /** Start date of the voucher in ISO 8601 format. */ startDate?: InputMaybe; /** Voucher type: PRODUCT, CATEGORY SHIPPING or ENTIRE_ORDER. */ @@ -7281,7 +7340,11 @@ export type VoucherInput = { }; export enum VoucherSortField { - /** Sort vouchers by code. */ + /** + * Sort vouchers by code. + * + * DEPRECATED: this field will be removed in Saleor 4.0. + */ CODE = 'CODE', /** Sort vouchers by end date. */ END_DATE = 'END_DATE', @@ -7291,6 +7354,12 @@ export enum VoucherSortField { * This option requires a channel filter to work as the values can vary between channels. */ MINIMUM_SPENT_AMOUNT = 'MINIMUM_SPENT_AMOUNT', + /** + * Sort vouchers by name. + * + * Added in Saleor 3.18. + */ + NAME = 'NAME', /** Sort vouchers by start date. */ START_DATE = 'START_DATE', /** Sort vouchers by type. */ @@ -7767,7 +7836,7 @@ export enum WebhookEventTypeAsyncEnum { PRODUCT_VARIANT_BACK_IN_STOCK = 'PRODUCT_VARIANT_BACK_IN_STOCK', /** A new product variant is created. */ PRODUCT_VARIANT_CREATED = 'PRODUCT_VARIANT_CREATED', - /** A product variant is deleted. */ + /** A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. */ PRODUCT_VARIANT_DELETED = 'PRODUCT_VARIANT_DELETED', /** * A product variant metadata is updated. @@ -7853,6 +7922,12 @@ export enum WebhookEventTypeAsyncEnum { TRANSLATION_CREATED = 'TRANSLATION_CREATED', /** A translation is updated. */ TRANSLATION_UPDATED = 'TRANSLATION_UPDATED', + /** + * A voucher code export is completed. + * + * Added in Saleor 3.18. + */ + VOUCHER_CODE_EXPORT_COMPLETED = 'VOUCHER_CODE_EXPORT_COMPLETED', /** A new voucher created. */ VOUCHER_CREATED = 'VOUCHER_CREATED', /** A voucher is deleted. */ @@ -8196,7 +8271,7 @@ export enum WebhookEventTypeEnum { PRODUCT_VARIANT_BACK_IN_STOCK = 'PRODUCT_VARIANT_BACK_IN_STOCK', /** A new product variant is created. */ PRODUCT_VARIANT_CREATED = 'PRODUCT_VARIANT_CREATED', - /** A product variant is deleted. */ + /** A product variant is deleted. Warning: this event will not be executed when parent product has been deleted. Check PRODUCT_DELETED. */ PRODUCT_VARIANT_DELETED = 'PRODUCT_VARIANT_DELETED', /** * A product variant metadata is updated. @@ -8311,6 +8386,12 @@ export enum WebhookEventTypeEnum { TRANSLATION_CREATED = 'TRANSLATION_CREATED', /** A translation is updated. */ TRANSLATION_UPDATED = 'TRANSLATION_UPDATED', + /** + * A voucher code export is completed. + * + * Added in Saleor 3.18. + */ + VOUCHER_CODE_EXPORT_COMPLETED = 'VOUCHER_CODE_EXPORT_COMPLETED', /** A new voucher created. */ VOUCHER_CREATED = 'VOUCHER_CREATED', /** A voucher is deleted. */ @@ -8537,6 +8618,7 @@ export enum WebhookSampleEventTypeEnum { TRANSACTION_ITEM_METADATA_UPDATED = 'TRANSACTION_ITEM_METADATA_UPDATED', TRANSLATION_CREATED = 'TRANSLATION_CREATED', TRANSLATION_UPDATED = 'TRANSLATION_UPDATED', + VOUCHER_CODE_EXPORT_COMPLETED = 'VOUCHER_CODE_EXPORT_COMPLETED', VOUCHER_CREATED = 'VOUCHER_CREATED', VOUCHER_DELETED = 'VOUCHER_DELETED', VOUCHER_METADATA_UPDATED = 'VOUCHER_METADATA_UPDATED', @@ -9414,7 +9496,7 @@ export type VoucherChannelListingUpdateMutationVariables = Exact<{ }>; -export type VoucherChannelListingUpdateMutation = { __typename: 'Mutation', voucherChannelListingUpdate: { __typename: 'VoucherChannelListingUpdate', errors: Array<{ __typename: 'DiscountError', code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', id: string, code: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; +export type VoucherChannelListingUpdateMutation = { __typename: 'Mutation', voucherChannelListingUpdate: { __typename: 'VoucherChannelListingUpdate', errors: Array<{ __typename: 'DiscountError', code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', id: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; export type VoucherUpdateMutationVariables = Exact<{ input: VoucherInput; @@ -9422,7 +9504,7 @@ export type VoucherUpdateMutationVariables = Exact<{ }>; -export type VoucherUpdateMutation = { __typename: 'Mutation', voucherUpdate: { __typename: 'VoucherUpdate', errors: Array<{ __typename: 'DiscountError', code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', id: string, code: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; +export type VoucherUpdateMutation = { __typename: 'Mutation', voucherUpdate: { __typename: 'VoucherUpdate', errors: Array<{ __typename: 'DiscountError', voucherCodes: Array | null, code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', id: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; export type VoucherCataloguesAddMutationVariables = Exact<{ input: CatalogueInput; @@ -9437,7 +9519,7 @@ export type VoucherCataloguesAddMutationVariables = Exact<{ }>; -export type VoucherCataloguesAddMutation = { __typename: 'Mutation', voucherCataloguesAdd: { __typename: 'VoucherAddCatalogues', errors: Array<{ __typename: 'DiscountError', code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', code: string, usageLimit: number | null, used: number, applyOncePerOrder: boolean, applyOncePerCustomer: boolean, onlyForStaff: boolean, id: string, name: string | null, startDate: any, endDate: any | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, productsCount: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, collectionsCount: { __typename: 'CollectionCountableConnection', totalCount: number | null } | null, categoriesCount: { __typename: 'CategoryCountableConnection', totalCount: number | null } | null, products?: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, collections?: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, categories?: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; +export type VoucherCataloguesAddMutation = { __typename: 'Mutation', voucherCataloguesAdd: { __typename: 'VoucherAddCatalogues', errors: Array<{ __typename: 'DiscountError', code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', usageLimit: number | null, used: number, applyOncePerOrder: boolean, applyOncePerCustomer: boolean, onlyForStaff: boolean, singleUse: boolean, id: string, name: string | null, startDate: any, endDate: any | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, productsCount: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, collectionsCount: { __typename: 'CollectionCountableConnection', totalCount: number | null } | null, categoriesCount: { __typename: 'CategoryCountableConnection', totalCount: number | null } | null, products?: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, collections?: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, categories?: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; export type VoucherCataloguesRemoveMutationVariables = Exact<{ input: CatalogueInput; @@ -9452,21 +9534,21 @@ export type VoucherCataloguesRemoveMutationVariables = Exact<{ }>; -export type VoucherCataloguesRemoveMutation = { __typename: 'Mutation', voucherCataloguesRemove: { __typename: 'VoucherRemoveCatalogues', errors: Array<{ __typename: 'DiscountError', code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', code: string, usageLimit: number | null, used: number, applyOncePerOrder: boolean, applyOncePerCustomer: boolean, onlyForStaff: boolean, id: string, name: string | null, startDate: any, endDate: any | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, productsCount: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, collectionsCount: { __typename: 'CollectionCountableConnection', totalCount: number | null } | null, categoriesCount: { __typename: 'CategoryCountableConnection', totalCount: number | null } | null, products?: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, collections?: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, categories?: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; +export type VoucherCataloguesRemoveMutation = { __typename: 'Mutation', voucherCataloguesRemove: { __typename: 'VoucherRemoveCatalogues', errors: Array<{ __typename: 'DiscountError', code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', usageLimit: number | null, used: number, applyOncePerOrder: boolean, applyOncePerCustomer: boolean, onlyForStaff: boolean, singleUse: boolean, id: string, name: string | null, startDate: any, endDate: any | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, productsCount: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, collectionsCount: { __typename: 'CollectionCountableConnection', totalCount: number | null } | null, categoriesCount: { __typename: 'CategoryCountableConnection', totalCount: number | null } | null, products?: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, collections?: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, categories?: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; export type VoucherCreateMutationVariables = Exact<{ input: VoucherInput; }>; -export type VoucherCreateMutation = { __typename: 'Mutation', voucherCreate: { __typename: 'VoucherCreate', errors: Array<{ __typename: 'DiscountError', code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', id: string, code: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; +export type VoucherCreateMutation = { __typename: 'Mutation', voucherCreate: { __typename: 'VoucherCreate', errors: Array<{ __typename: 'DiscountError', voucherCodes: Array | null, code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }>, voucher: { __typename: 'Voucher', id: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null } | null }; export type VoucherDeleteMutationVariables = Exact<{ id: Scalars['ID']; }>; -export type VoucherDeleteMutation = { __typename: 'Mutation', voucherDelete: { __typename: 'VoucherDelete', errors: Array<{ __typename: 'DiscountError', code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }> } | null }; +export type VoucherDeleteMutation = { __typename: 'Mutation', voucherDelete: { __typename: 'VoucherDelete', errors: Array<{ __typename: 'DiscountError', voucherCodes: Array | null, code: DiscountErrorCode, field: string | null, channels: Array | null, message: string | null }> } | null }; export type VoucherBulkDeleteMutationVariables = Exact<{ ids: Array | Scalars['ID']; @@ -9499,7 +9581,7 @@ export type VoucherListQueryVariables = Exact<{ }>; -export type VoucherListQuery = { __typename: 'Query', vouchers: { __typename: 'VoucherCountableConnection', edges: Array<{ __typename: 'VoucherCountableEdge', node: { __typename: 'Voucher', id: string, code: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null }; +export type VoucherListQuery = { __typename: 'Query', vouchers: { __typename: 'VoucherCountableConnection', edges: Array<{ __typename: 'VoucherCountableEdge', node: { __typename: 'Voucher', id: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null }; export type SaleDetailsQueryVariables = Exact<{ id: Scalars['ID']; @@ -9528,7 +9610,18 @@ export type VoucherDetailsQueryVariables = Exact<{ }>; -export type VoucherDetailsQuery = { __typename: 'Query', voucher: { __typename: 'Voucher', code: string, usageLimit: number | null, used: number, applyOncePerOrder: boolean, applyOncePerCustomer: boolean, onlyForStaff: boolean, id: string, name: string | null, startDate: any, endDate: any | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, productsCount: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, collectionsCount: { __typename: 'CollectionCountableConnection', totalCount: number | null } | null, categoriesCount: { __typename: 'CategoryCountableConnection', totalCount: number | null } | null, products?: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, collections?: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, categories?: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null }; +export type VoucherDetailsQuery = { __typename: 'Query', voucher: { __typename: 'Voucher', usageLimit: number | null, used: number, applyOncePerOrder: boolean, applyOncePerCustomer: boolean, onlyForStaff: boolean, singleUse: boolean, id: string, name: string | null, startDate: any, endDate: any | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, productsCount: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, collectionsCount: { __typename: 'CollectionCountableConnection', totalCount: number | null } | null, categoriesCount: { __typename: 'CategoryCountableConnection', totalCount: number | null } | null, products?: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, collections?: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, categories?: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> } | null }; + +export type VoucherCodesQueryVariables = Exact<{ + id: Scalars['ID']; + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}>; + + +export type VoucherCodesQuery = { __typename: 'Query', voucher: { __typename: 'Voucher', codes: { __typename: 'VoucherCodeCountableConnection', edges: Array<{ __typename: 'VoucherCodeCountableEdge', node: { __typename: 'VoucherCode', code: string | null, used: number | null, isActive: boolean | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null } | null }; export type FileUploadMutationVariables = Exact<{ file: Scalars['Upload']; @@ -9599,9 +9692,11 @@ export type SaleFragment = { __typename: 'Sale', id: string, name: string, type: export type SaleDetailsFragment = { __typename: 'Sale', id: string, name: string, type: SaleType, startDate: any, endDate: any | null, variantsCount: { __typename: 'ProductVariantCountableConnection', totalCount: number | null } | null, productsCount: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, collectionsCount: { __typename: 'CollectionCountableConnection', totalCount: number | null } | null, categoriesCount: { __typename: 'CategoryCountableConnection', totalCount: number | null } | null, variants?: { __typename: 'ProductVariantCountableConnection', edges: Array<{ __typename: 'ProductVariantCountableEdge', node: { __typename: 'ProductVariant', id: string, name: string, product: { __typename: 'Product', id: string, name: string, thumbnail: { __typename: 'Image', url: string } | null, productType: { __typename: 'ProductType', id: string, name: string }, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, products?: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, categories?: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, collections?: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, channelListings: Array<{ __typename: 'SaleChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; -export type VoucherFragment = { __typename: 'Voucher', id: string, code: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; +export type VoucherFragment = { __typename: 'Voucher', id: string, name: string | null, startDate: any, endDate: any | null, usageLimit: number | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; + +export type VoucherCodeFragment = { __typename: 'VoucherCode', code: string | null, used: number | null, isActive: boolean | null }; -export type VoucherDetailsFragment = { __typename: 'Voucher', code: string, usageLimit: number | null, used: number, applyOncePerOrder: boolean, applyOncePerCustomer: boolean, onlyForStaff: boolean, id: string, name: string | null, startDate: any, endDate: any | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, productsCount: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, collectionsCount: { __typename: 'CollectionCountableConnection', totalCount: number | null } | null, categoriesCount: { __typename: 'CategoryCountableConnection', totalCount: number | null } | null, products?: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, collections?: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, categories?: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; +export type VoucherDetailsFragment = { __typename: 'Voucher', usageLimit: number | null, used: number, applyOncePerOrder: boolean, applyOncePerCustomer: boolean, onlyForStaff: boolean, singleUse: boolean, id: string, name: string | null, startDate: any, endDate: any | null, type: VoucherTypeEnum, discountValueType: DiscountValueTypeEnum, minCheckoutItemsQuantity: number | null, productsCount: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, collectionsCount: { __typename: 'CollectionCountableConnection', totalCount: number | null } | null, categoriesCount: { __typename: 'CategoryCountableConnection', totalCount: number | null } | null, products?: { __typename: 'ProductCountableConnection', edges: Array<{ __typename: 'ProductCountableEdge', node: { __typename: 'Product', id: string, name: string, productType: { __typename: 'ProductType', id: string, name: string }, thumbnail: { __typename: 'Image', url: string } | null, channelListings: Array<{ __typename: 'ProductChannelListing', isPublished: boolean, publicationDate: any | null, isAvailableForPurchase: boolean | null, availableForPurchase: any | null, visibleInListings: boolean, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string } }> | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, collections?: { __typename: 'CollectionCountableConnection', edges: Array<{ __typename: 'CollectionCountableEdge', node: { __typename: 'Collection', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, categories?: { __typename: 'CategoryCountableConnection', edges: Array<{ __typename: 'CategoryCountableEdge', node: { __typename: 'Category', id: string, name: string, products: { __typename: 'ProductCountableConnection', totalCount: number | null } | null } }>, pageInfo: { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }> | null, channelListings: Array<{ __typename: 'VoucherChannelListing', id: string, discountValue: number, currency: string, channel: { __typename: 'Channel', id: string, name: string, currencyCode: string }, minSpent: { __typename: 'Money', amount: number, currency: string } | null }> | null, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }> }; export type AttributeErrorFragment = { __typename: 'AttributeError', code: AttributeErrorCode, field: string | null, message: string | null }; @@ -9739,9 +9834,9 @@ export type TransactionRequestActionErrorFragment = { __typename: 'TransactionRe export type TransactionCreateErrorFragment = { __typename: 'TransactionCreateError', field: string | null, message: string | null, code: TransactionCreateErrorCode }; -export type OrderGrantRefundCreateErrorFragment = { __typename: 'OrderGrantRefundCreateError', field: string | null, message: string | null, code: OrderGrantRefundCreateErrorCode }; +export type OrderGrantRefundCreateErrorFragment = { __typename: 'OrderGrantRefundCreateError', field: string | null, message: string | null, code: OrderGrantRefundCreateErrorCode, lines: Array<{ __typename: 'OrderGrantRefundCreateLineError', field: string | null, message: string | null, code: OrderGrantRefundCreateLineErrorCode, lineId: string }> | null }; -export type OrderGrantRefundUpdateErrorFragment = { __typename: 'OrderGrantRefundUpdateError', field: string | null, message: string | null, code: OrderGrantRefundUpdateErrorCode }; +export type OrderGrantRefundUpdateErrorFragment = { __typename: 'OrderGrantRefundUpdateError', field: string | null, message: string | null, code: OrderGrantRefundUpdateErrorCode, addLines: Array<{ __typename: 'OrderGrantRefundUpdateLineError', field: string | null, message: string | null, code: OrderGrantRefundUpdateLineErrorCode, lineId: string }> | null, removeLines: Array<{ __typename: 'OrderGrantRefundUpdateLineError', field: string | null, message: string | null, code: OrderGrantRefundUpdateLineErrorCode, lineId: string }> | null }; export type FileFragment = { __typename: 'File', url: string, contentType: string | null }; @@ -9873,9 +9968,11 @@ export type OrderGrantedRefundFragment = { __typename: 'OrderGrantedRefund', id: export type OrderLineGrantRefundFragment = { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }; +export type OrderDetailsGrantedRefundFragment = { __typename: 'OrderGrantedRefund', id: string, reason: string | null, shippingCostsIncluded: boolean, amount: { __typename: 'Money', amount: number, currency: string }, lines: Array<{ __typename: 'OrderGrantedRefundLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, isShippingRequired: boolean, productName: string, productSku: string | null, quantity: number, quantityFulfilled: number, quantityToFulfill: number, unitDiscountValue: any, unitDiscountReason: string | null, unitDiscountType: DiscountValueTypeEnum | null, allocations: Array<{ __typename: 'Allocation', id: string, quantity: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, variant: { __typename: 'ProductVariant', id: string, name: string, quantityAvailable: number | null, preorder: { __typename: 'PreorderData', endDate: any | null } | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, product: { __typename: 'Product', id: string, isAvailableForPurchase: boolean | null } } | null, totalPrice: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string }, gross: { __typename: 'Money', amount: number, currency: string } }, unitDiscount: { __typename: 'Money', amount: number, currency: string }, undiscountedUnitPrice: { __typename: 'TaxedMoney', currency: string, gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, thumbnail: { __typename: 'Image', url: string } | null } }> | null }; + export type OrderFulfillmentGrantRefundFragment = { __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }; -export type OrderDetailsGrantRefundFragment = { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }; +export type OrderDetailsGrantRefundFragment = { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, grantedRefunds: Array<{ __typename: 'OrderGrantedRefund', id: string, reason: string | null, shippingCostsIncluded: boolean, amount: { __typename: 'Money', amount: number, currency: string }, lines: Array<{ __typename: 'OrderGrantedRefundLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, isShippingRequired: boolean, productName: string, productSku: string | null, quantity: number, quantityFulfilled: number, quantityToFulfill: number, unitDiscountValue: any, unitDiscountReason: string | null, unitDiscountType: DiscountValueTypeEnum | null, allocations: Array<{ __typename: 'Allocation', id: string, quantity: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, variant: { __typename: 'ProductVariant', id: string, name: string, quantityAvailable: number | null, preorder: { __typename: 'PreorderData', endDate: any | null } | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, product: { __typename: 'Product', id: string, isAvailableForPurchase: boolean | null } } | null, totalPrice: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string }, gross: { __typename: 'Money', amount: number, currency: string } }, unitDiscount: { __typename: 'Money', amount: number, currency: string }, undiscountedUnitPrice: { __typename: 'TaxedMoney', currency: string, gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, thumbnail: { __typename: 'Image', url: string } | null } }> | null }> }; export type PageInfoFragment = { __typename: 'PageInfo', endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean, startCursor: string | null }; @@ -10180,15 +10277,37 @@ export type CustomerGiftCardListQueryVariables = Exact<{ export type CustomerGiftCardListQuery = { __typename: 'Query', giftCards: { __typename: 'GiftCardCountableConnection', edges: Array<{ __typename: 'GiftCardCountableEdge', node: { __typename: 'GiftCard', id: string, last4CodeChars: string, expiryDate: any | null, isActive: boolean, currentBalance: { __typename: 'Money', amount: number, currency: string } } }> } | null }; -export type HomeQueryVariables = Exact<{ +export type HomeAnaliticsQueryVariables = Exact<{ channel: Scalars['String']; datePeriod: DateRangeInput; + hasPermissionToManageOrders: Scalars['Boolean']; +}>; + + +export type HomeAnaliticsQuery = { __typename: 'Query', salesToday: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } | null, ordersToday: { __typename: 'OrderCountableConnection', totalCount: number | null } | null }; + +export type HomeActivitiesQueryVariables = Exact<{ + hasPermissionToManageOrders: Scalars['Boolean']; +}>; + + +export type HomeActivitiesQuery = { __typename: 'Query', activities: { __typename: 'OrderEventCountableConnection', edges: Array<{ __typename: 'OrderEventCountableEdge', node: { __typename: 'OrderEvent', amount: number | null, composedId: string | null, date: any | null, email: string | null, emailType: OrderEventsEmailsEnum | null, id: string, message: string | null, orderNumber: string | null, oversoldItems: Array | null, quantity: number | null, type: OrderEventsEnum | null, user: { __typename: 'User', id: string, email: string } | null } }> } | null }; + +export type HomeTopProductsQueryVariables = Exact<{ + channel: Scalars['String']; hasPermissionToManageProducts: Scalars['Boolean']; +}>; + + +export type HomeTopProductsQuery = { __typename: 'Query', productTopToday: { __typename: 'ProductVariantCountableConnection', edges: Array<{ __typename: 'ProductVariantCountableEdge', node: { __typename: 'ProductVariant', id: string, quantityOrdered: number | null, revenue: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } | null, attributes: Array<{ __typename: 'SelectedAttribute', values: Array<{ __typename: 'AttributeValue', id: string, name: string | null }> }>, product: { __typename: 'Product', id: string, name: string, thumbnail: { __typename: 'Image', url: string } | null } } }> } | null }; + +export type HomeNotificationsQueryVariables = Exact<{ + channel: Scalars['String']; hasPermissionToManageOrders: Scalars['Boolean']; }>; -export type HomeQuery = { __typename: 'Query', salesToday: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } | null, ordersToday: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, ordersToFulfill: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, ordersToCapture: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, productsOutOfStock: { __typename: 'ProductCountableConnection', totalCount: number | null } | null, productTopToday: { __typename: 'ProductVariantCountableConnection', edges: Array<{ __typename: 'ProductVariantCountableEdge', node: { __typename: 'ProductVariant', id: string, quantityOrdered: number | null, revenue: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } | null, attributes: Array<{ __typename: 'SelectedAttribute', values: Array<{ __typename: 'AttributeValue', id: string, name: string | null }> }>, product: { __typename: 'Product', id: string, name: string, thumbnail: { __typename: 'Image', url: string } | null } } }> } | null, activities: { __typename: 'OrderEventCountableConnection', edges: Array<{ __typename: 'OrderEventCountableEdge', node: { __typename: 'OrderEvent', amount: number | null, composedId: string | null, date: any | null, email: string | null, emailType: OrderEventsEmailsEnum | null, id: string, message: string | null, orderNumber: string | null, oversoldItems: Array | null, quantity: number | null, type: OrderEventsEnum | null, user: { __typename: 'User', id: string, email: string } | null } }> } | null }; +export type HomeNotificationsQuery = { __typename: 'Query', ordersToFulfill: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, ordersToCapture: { __typename: 'OrderCountableConnection', totalCount: number | null } | null, productsOutOfStock: { __typename: 'ProductCountableConnection', totalCount: number | null } | null }; export type MenuCreateMutationVariables = Exact<{ input: MenuCreateInput; @@ -10501,21 +10620,26 @@ export type OrderTransactionRequestActionMutation = { __typename: 'Mutation', tr export type OrderGrantRefundAddMutationVariables = Exact<{ orderId: Scalars['ID']; - amount: Scalars['Decimal']; + amount?: InputMaybe; reason?: InputMaybe; + lines?: InputMaybe | OrderGrantRefundCreateLineInput>; + grantRefundForShipping?: InputMaybe; }>; -export type OrderGrantRefundAddMutation = { __typename: 'Mutation', orderGrantRefundCreate: { __typename: 'OrderGrantRefundCreate', errors: Array<{ __typename: 'OrderGrantRefundCreateError', field: string | null, message: string | null, code: OrderGrantRefundCreateErrorCode }> } | null }; +export type OrderGrantRefundAddMutation = { __typename: 'Mutation', orderGrantRefundCreate: { __typename: 'OrderGrantRefundCreate', errors: Array<{ __typename: 'OrderGrantRefundCreateError', field: string | null, message: string | null, code: OrderGrantRefundCreateErrorCode, lines: Array<{ __typename: 'OrderGrantRefundCreateLineError', field: string | null, message: string | null, code: OrderGrantRefundCreateLineErrorCode, lineId: string }> | null }> } | null }; export type OrderGrantRefundEditMutationVariables = Exact<{ refundId: Scalars['ID']; - amount: Scalars['Decimal']; + amount?: InputMaybe; reason?: InputMaybe; + addLines?: InputMaybe | OrderGrantRefundUpdateLineAddInput>; + removeLines?: InputMaybe | Scalars['ID']>; + grantRefundForShipping?: InputMaybe; }>; -export type OrderGrantRefundEditMutation = { __typename: 'Mutation', orderGrantRefundUpdate: { __typename: 'OrderGrantRefundUpdate', errors: Array<{ __typename: 'OrderGrantRefundUpdateError', field: string | null, message: string | null, code: OrderGrantRefundUpdateErrorCode }> } | null }; +export type OrderGrantRefundEditMutation = { __typename: 'Mutation', orderGrantRefundUpdate: { __typename: 'OrderGrantRefundUpdate', errors: Array<{ __typename: 'OrderGrantRefundUpdateError', field: string | null, message: string | null, code: OrderGrantRefundUpdateErrorCode, addLines: Array<{ __typename: 'OrderGrantRefundUpdateLineError', field: string | null, message: string | null, code: OrderGrantRefundUpdateLineErrorCode, lineId: string }> | null, removeLines: Array<{ __typename: 'OrderGrantRefundUpdateLineError', field: string | null, message: string | null, code: OrderGrantRefundUpdateLineErrorCode, lineId: string }> | null }> } | null }; export type OrderSendRefundMutationVariables = Exact<{ amount: Scalars['PositiveDecimal']; @@ -10591,14 +10715,14 @@ export type OrderDetailsGrantRefundQueryVariables = Exact<{ }>; -export type OrderDetailsGrantRefundQuery = { __typename: 'Query', order: { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }; +export type OrderDetailsGrantRefundQuery = { __typename: 'Query', order: { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, grantedRefunds: Array<{ __typename: 'OrderGrantedRefund', id: string, reason: string | null, shippingCostsIncluded: boolean, amount: { __typename: 'Money', amount: number, currency: string }, lines: Array<{ __typename: 'OrderGrantedRefundLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, isShippingRequired: boolean, productName: string, productSku: string | null, quantity: number, quantityFulfilled: number, quantityToFulfill: number, unitDiscountValue: any, unitDiscountReason: string | null, unitDiscountType: DiscountValueTypeEnum | null, allocations: Array<{ __typename: 'Allocation', id: string, quantity: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, variant: { __typename: 'ProductVariant', id: string, name: string, quantityAvailable: number | null, preorder: { __typename: 'PreorderData', endDate: any | null } | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, product: { __typename: 'Product', id: string, isAvailableForPurchase: boolean | null } } | null, totalPrice: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string }, gross: { __typename: 'Money', amount: number, currency: string } }, unitDiscount: { __typename: 'Money', amount: number, currency: string }, undiscountedUnitPrice: { __typename: 'TaxedMoney', currency: string, gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, thumbnail: { __typename: 'Image', url: string } | null } }> | null }> } | null }; export type OrderDetailsGrantRefundEditQueryVariables = Exact<{ id: Scalars['ID']; }>; -export type OrderDetailsGrantRefundEditQuery = { __typename: 'Query', order: { __typename: 'Order', id: string, number: string, grantedRefunds: Array<{ __typename: 'OrderGrantedRefund', id: string, reason: string | null, amount: { __typename: 'Money', amount: number, currency: string } }>, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }; +export type OrderDetailsGrantRefundEditQuery = { __typename: 'Query', order: { __typename: 'Order', id: string, number: string, lines: Array<{ __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } }>, fulfillments: Array<{ __typename: 'Fulfillment', id: string, fulfillmentOrder: number, status: FulfillmentStatus, lines: Array<{ __typename: 'FulfillmentLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, productName: string, quantity: number, quantityToFulfill: number, variantName: string, thumbnail: { __typename: 'Image', url: string } | null, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } } } | null }> | null }>, shippingPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, total: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string } }, grantedRefunds: Array<{ __typename: 'OrderGrantedRefund', id: string, reason: string | null, shippingCostsIncluded: boolean, amount: { __typename: 'Money', amount: number, currency: string }, lines: Array<{ __typename: 'OrderGrantedRefundLine', id: string, quantity: number, orderLine: { __typename: 'OrderLine', id: string, isShippingRequired: boolean, productName: string, productSku: string | null, quantity: number, quantityFulfilled: number, quantityToFulfill: number, unitDiscountValue: any, unitDiscountReason: string | null, unitDiscountType: DiscountValueTypeEnum | null, allocations: Array<{ __typename: 'Allocation', id: string, quantity: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, variant: { __typename: 'ProductVariant', id: string, name: string, quantityAvailable: number | null, preorder: { __typename: 'PreorderData', endDate: any | null } | null, stocks: Array<{ __typename: 'Stock', id: string, quantity: number, quantityAllocated: number, warehouse: { __typename: 'Warehouse', id: string, name: string } }> | null, product: { __typename: 'Product', id: string, isAvailableForPurchase: boolean | null } } | null, totalPrice: { __typename: 'TaxedMoney', net: { __typename: 'Money', amount: number, currency: string }, gross: { __typename: 'Money', amount: number, currency: string } }, unitDiscount: { __typename: 'Money', amount: number, currency: string }, undiscountedUnitPrice: { __typename: 'TaxedMoney', currency: string, gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, unitPrice: { __typename: 'TaxedMoney', gross: { __typename: 'Money', amount: number, currency: string }, net: { __typename: 'Money', amount: number, currency: string } }, thumbnail: { __typename: 'Image', url: string } | null } }> | null }> } | null }; export type OrderFulfillDataQueryVariables = Exact<{ orderId: Scalars['ID']; diff --git a/src/home/components/HomeActivityCard/HomeActivityCard.tsx b/src/home/components/HomeActivityCard/HomeActivityCard.tsx index 7877d1bd29c..1abfebf0499 100644 --- a/src/home/components/HomeActivityCard/HomeActivityCard.tsx +++ b/src/home/components/HomeActivityCard/HomeActivityCard.tsx @@ -1,8 +1,7 @@ import { DashboardCard } from "@dashboard/components/Card"; import { DateTime } from "@dashboard/components/Date"; -import Skeleton from "@dashboard/components/Skeleton"; -import { Activities } from "@dashboard/home/types"; -import { Box, List, Text, useTheme } from "@saleor/macaw-ui-next"; +import { Activities, HomeData } from "@dashboard/home/types"; +import { Box, List, Skeleton, Text, useTheme } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -10,7 +9,7 @@ import { renderCollection } from "../../../misc"; import { getActivityMessage } from "./activityMessages"; interface HomeActivityCardProps { - activities: Activities; + activities: HomeData; testId?: string; } @@ -20,20 +19,50 @@ export const HomeActivityCard = ({ }: HomeActivityCardProps) => { const intl = useIntl(); const { themeValues } = useTheme(); + const title = intl.formatMessage({ + id: "BXkF8Z", + defaultMessage: "Activity", + description: "header", + }); + + if (activities.hasError) { + return ( + + {title} + + + + + + + ); + } + + if (activities.loading) { + return ( + + {title} + + + + + + + + + ); + } return ( - - {intl.formatMessage({ - id: "BXkF8Z", - defaultMessage: "Activity", - description: "header", - })} - + {title} {renderCollection( - activities, + activities.data, (activity, activityId) => ( { switch (activity.type) { diff --git a/src/home/components/HomeNotificationList/HomeNotificationList.tsx b/src/home/components/HomeNotificationList/HomeNotificationList.tsx index 44caa9c31b6..61f239040b8 100644 --- a/src/home/components/HomeNotificationList/HomeNotificationList.tsx +++ b/src/home/components/HomeNotificationList/HomeNotificationList.tsx @@ -1,5 +1,6 @@ import RequirePermissions from "@dashboard/components/RequirePermissions"; import { PermissionEnum } from "@dashboard/graphql"; +import { HomeData, Notifications } from "@dashboard/home/types"; import { List } from "@saleor/macaw-ui-next"; import React from "react"; import { useIntl } from "react-intl"; @@ -13,9 +14,7 @@ import { } from "./utils"; interface HomeNotificationTableProps { - ordersToCapture: number; - ordersToFulfill: number; - productsOutOfStock: number; + notifications: HomeData; createNewChannelHref: string; ordersToFulfillHref: string; ordersToCaptureHref: string; @@ -24,24 +23,30 @@ interface HomeNotificationTableProps { } export const HomeNotificationList = ({ + notifications, createNewChannelHref, ordersToFulfillHref, ordersToCaptureHref, productsOutOfStockHref, - ordersToCapture, - ordersToFulfill, - productsOutOfStock, + noChannel, }: HomeNotificationTableProps) => { const intl = useIntl(); + if (notifications.hasError) { + return null; + } + return ( {noChannel && ( - + {intl.formatMessage(messages.createNewChannel)} @@ -49,17 +54,22 @@ export const HomeNotificationList = ({ - {getOrderToFulfillText(ordersToFulfill, intl)} + {getOrderToFulfillText(notifications.data.ordersToFulfill ?? 0, intl)} - {getOrdersToCaptureText(ordersToCapture, intl)} + {getOrdersToCaptureText( + notifications.data.ordersToCapture ?? 0, + intl, + )} @@ -67,10 +77,14 @@ export const HomeNotificationList = ({ requiredPermissions={[PermissionEnum.MANAGE_PRODUCTS]} > - {getProductsOutOfStockText(productsOutOfStock, intl)} + {getProductsOutOfStockText( + notifications.data.productsOutOfStock ?? 0, + intl, + )} diff --git a/src/home/components/HomeNotificationList/HomeNotificationListItem.tsx b/src/home/components/HomeNotificationList/HomeNotificationListItem.tsx index ac7a202e035..712eabe908e 100644 --- a/src/home/components/HomeNotificationList/HomeNotificationListItem.tsx +++ b/src/home/components/HomeNotificationList/HomeNotificationListItem.tsx @@ -2,6 +2,7 @@ import { Box, ChevronRightIcon, List, + Skeleton, sprinkles, Text, } from "@saleor/macaw-ui-next"; @@ -12,32 +13,60 @@ interface HomeNotificationListItemProps { dataTestId?: string; linkUrl: string; children: ReactNode; + loading: boolean; } export const HomeNotificationListItem = ({ dataTestId, linkUrl, children, -}: HomeNotificationListItemProps) => ( - - - { + if (loading) { + return ( + + + + + + ); + } + + return ( + + - {children} - - - - -); + + {children} + + + + + ); +}; + +function Listitem({ + children, + dataTestId, +}: { + children: ReactNode; + dataTestId?: string; +}) { + return ( + + {children} + + ); +} diff --git a/src/home/components/HomePage/HomePage.stories.tsx b/src/home/components/HomePage/HomePage.stories.tsx index 37a5bf7e8b9..598f2b8f1ec 100644 --- a/src/home/components/HomePage/HomePage.stories.tsx +++ b/src/home/components/HomePage/HomePage.stories.tsx @@ -2,28 +2,53 @@ import placeholderImage from "@assets/images/placeholder60x60.png"; import { adminUserPermissions } from "@dashboard/fixtures"; import { PermissionEnum } from "@dashboard/graphql"; -import { shop as shopFixture } from "@dashboard/home/fixtures"; +import { + activities, + analitics, + notifications, + topProducts as topProductsFixture, +} from "@dashboard/home/fixtures"; import { mapEdgesToItems } from "@dashboard/utils/maps"; import React from "react"; import { MockedUserProvider } from "../../../../.storybook/helpers"; import HomePageComponent, { HomePageProps } from "./HomePage"; -const shop = shopFixture(placeholderImage); +const productTopToday = topProductsFixture(placeholderImage); const homePageProps: Omit = { - activities: mapEdgesToItems(shop.activities), + activities: { + data: mapEdgesToItems(activities), + loading: false, + hasError: false, + }, + notifications: { + data: { + ordersToCapture: notifications.ordersToCapture.totalCount, + ordersToFulfill: notifications.ordersToFulfill.totalCount, + productsOutOfStock: notifications.productsOutOfStock.totalCount, + }, + loading: false, + hasError: false, + }, + analitics: { + data: { + orders: analitics.ordersToday.totalCount, + sales: analitics.salesToday.gross, + }, + loading: false, + hasError: false, + }, noChannel: false, createNewChannelHref: "", ordersToFulfillHref: "", ordersToCaptureHref: "", productsOutOfStockHref: "", - orders: shop.ordersToday.totalCount, - ordersToCapture: shop.ordersToCapture.totalCount, - ordersToFulfill: shop.ordersToFulfill.totalCount, - productsOutOfStock: shop.productsOutOfStock.totalCount, - sales: shop.salesToday.gross, - topProducts: mapEdgesToItems(shop.productTopToday), + topProducts: { + data: mapEdgesToItems(productTopToday), + loading: false, + hasError: false, + }, userName: "admin@example.com", }; @@ -46,19 +71,69 @@ export const Default = () => ; export const Loading = () => ( +); + +export const Error = () => ( + ); export const NoData = () => ( - + ); export const NoPermissions = () => ( diff --git a/src/home/components/HomePage/HomePage.tsx b/src/home/components/HomePage/HomePage.tsx index 6ac5b2a0821..56e8eb47ce2 100644 --- a/src/home/components/HomePage/HomePage.tsx +++ b/src/home/components/HomePage/HomePage.tsx @@ -3,10 +3,15 @@ import CardSpacer from "@dashboard/components/CardSpacer"; import { DetailPageLayout } from "@dashboard/components/Layouts"; import Money from "@dashboard/components/Money"; import RequirePermissions from "@dashboard/components/RequirePermissions"; -import Skeleton from "@dashboard/components/Skeleton"; -import { HomeQuery, PermissionEnum } from "@dashboard/graphql"; -import { Activities, ProductTopToday } from "@dashboard/home/types"; -import { Box } from "@saleor/macaw-ui-next"; +import { PermissionEnum } from "@dashboard/graphql"; +import { + Activities, + Analitics, + HomeData, + Notifications, + ProductTopToday, +} from "@dashboard/home/types"; +import { Box, Skeleton } from "@saleor/macaw-ui-next"; import React from "react"; import { useIntl } from "react-intl"; @@ -18,13 +23,10 @@ import { HomeProductList } from "../HomeProductList"; import { homePageMessages } from "./messages"; export interface HomePageProps { - activities: Activities; - orders: number | null; - ordersToCapture: number | null; - ordersToFulfill: number | null; - productsOutOfStock: number; - sales: NonNullable["gross"]; - topProducts: ProductTopToday | null; + activities: HomeData; + analitics: HomeData; + topProducts: HomeData; + notifications: HomeData; userName: string; createNewChannelHref: string; ordersToFulfillHref: string; @@ -36,17 +38,14 @@ export interface HomePageProps { const HomePage: React.FC = props => { const { userName, - orders, - sales, + analitics, topProducts, activities, createNewChannelHref, ordersToFulfillHref, ordersToCaptureHref, productsOutOfStockHref, - ordersToCapture = 0, - ordersToFulfill = 0, - productsOutOfStock = 0, + notifications, noChannel, } = props; const intl = useIntl(); @@ -70,10 +69,10 @@ const HomePage: React.FC = props => { title={intl.formatMessage(homePageMessages.salesCardTitle)} testId="sales-analytics" > - {noChannel ? ( + {noChannel || analitics.hasError ? ( 0 - ) : sales ? ( - + ) : !analitics.loading ? ( + ) : ( )} @@ -82,10 +81,10 @@ const HomePage: React.FC = props => { title={intl.formatMessage(homePageMessages.ordersCardTitle)} testId="orders-analytics" > - {noChannel ? ( + {noChannel || analitics.hasError ? ( 0 - ) : orders !== undefined ? ( - orders + ) : !analitics.loading ? ( + analitics.data.orders ) : ( )} @@ -97,9 +96,7 @@ const HomePage: React.FC = props => { ordersToFulfillHref={ordersToFulfillHref} ordersToCaptureHref={ordersToCaptureHref} productsOutOfStockHref={productsOutOfStockHref} - ordersToCapture={ordersToCapture ?? 0} - ordersToFulfill={ordersToFulfill ?? 0} - productsOutOfStock={productsOutOfStock} + notifications={notifications} noChannel={noChannel} /> diff --git a/src/home/components/HomeProductList/HomeProductList.tsx b/src/home/components/HomeProductList/HomeProductList.tsx index d82a7c08060..263e96d6483 100644 --- a/src/home/components/HomeProductList/HomeProductList.tsx +++ b/src/home/components/HomeProductList/HomeProductList.tsx @@ -1,8 +1,7 @@ import Money from "@dashboard/components/Money"; -import Skeleton from "@dashboard/components/Skeleton"; -import { ProductTopToday } from "@dashboard/home/types"; +import { HomeData, ProductTopToday } from "@dashboard/home/types"; import { productVariantEditUrl } from "@dashboard/products/urls"; -import { Box, Text } from "@saleor/macaw-ui-next"; +import { Box, Skeleton, Text } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -12,7 +11,7 @@ import { generateAttributesInfo } from "./variant"; interface HomeProductListProps { testId?: string; - topProducts: ProductTopToday; + topProducts: HomeData; } export const HomeProductList = ({ @@ -20,19 +19,51 @@ export const HomeProductList = ({ testId, }: HomeProductListProps) => { const intl = useIntl(); + const title = intl.formatMessage({ + id: "e08xWz", + defaultMessage: "Top products", + description: "header", + }); + + if (topProducts.hasError) { + return ( + + + {title} + + + + + + ); + } + + if (topProducts.loading) { + return ( + + + {title} + + + + + + + + ); + } return ( - {intl.formatMessage({ - id: "e08xWz", - defaultMessage: "Top products", - description: "header", - })} + {title} {renderCollection( - topProducts, + topProducts.data, variant => ( + + + ); +} diff --git a/src/home/fixtures.ts b/src/home/fixtures.ts index d357a16010a..49836570245 100644 --- a/src/home/fixtures.ts +++ b/src/home/fixtures.ts @@ -1,430 +1,439 @@ // @ts-strict-ignore -import { HomeQuery, OrderEventsEnum } from "@dashboard/graphql"; +import { + HomeActivitiesQuery, + HomeAnaliticsQuery, + HomeNotificationsQuery, + HomeTopProductsQuery, + OrderEventsEnum, +} from "@dashboard/graphql"; -export const shop: (placeholderImage: string) => HomeQuery = ( - placeholderImage: string, -) => ({ +export const notifications: HomeNotificationsQuery = { __typename: "Query", - activities: { - __typename: "OrderEventCountableConnection", - edges: [ - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-09-14T16:10:27.137126+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDoxOA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + ordersToCapture: { + __typename: "OrderCountableConnection", + totalCount: 0, + }, + ordersToFulfill: { + __typename: "OrderCountableConnection", + totalCount: 1, + }, + + productsOutOfStock: { + __typename: "ProductCountableConnection", + totalCount: 0, + }, +}; + +export const activities: HomeActivitiesQuery["activities"] = { + __typename: "OrderEventCountableConnection", + edges: [ + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-09-14T16:10:27.137126+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDoxOA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-03T13:28:46.325279+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDozNQ==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-03T13:28:46.325279+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDozNQ==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-03T13:29:01.837496+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDozNw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.ORDER_FULLY_PAID, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-03T13:29:01.837496+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDozNw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.ORDER_FULLY_PAID, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-04T01:01:51.243723+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo1OA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-04T01:01:51.243723+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo1OA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-04T19:36:18.831561+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo2Nw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-04T19:36:18.831561+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo2Nw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-04T19:38:01.420365+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo2OA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-04T19:38:01.420365+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo2OA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-05T12:30:57.268592+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3MQ==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-05T12:30:57.268592+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3MQ==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-08T09:50:42.622253+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3Mw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-08T09:50:42.622253+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3Mw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-12T15:51:11.665838+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3Nw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: "admin@example.com", - id: "VXNlcjoyMQ==", - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-12T15:51:11.665838+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3Nw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", + email: "admin@example.com", + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-12T15:51:11.665838+00:00", + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-12T15:51:11.665838+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3Nw==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED_FROM_DRAFT, + user: { + __typename: "User", email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3Nw==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED_FROM_DRAFT, - user: { - __typename: "User", - email: null, - id: "VXNlcjoyMQ==", - }, + id: "VXNlcjoyMQ==", }, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-25T11:25:58.843860+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo3OA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-25T11:25:58.843860+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo3OA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-26T09:34:57.580167+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo4MA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.PLACED, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-26T09:34:57.580167+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo4MA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.PLACED, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-26T09:38:02.440061+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo4Mg==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.ORDER_FULLY_PAID, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-26T09:38:02.440061+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo4Mg==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.ORDER_FULLY_PAID, + user: null, }, - { - __typename: "OrderEventCountableEdge", - node: { - __typename: "OrderEvent", - amount: null, - composedId: null, - date: "2018-10-26T09:38:02.467443+00:00", - email: null, - emailType: null, - id: "T3JkZXJFdmVudDo4NA==", - message: null, - orderNumber: "15", - oversoldItems: null, - quantity: null, - type: OrderEventsEnum.ORDER_FULLY_PAID, - user: null, - }, + }, + { + __typename: "OrderEventCountableEdge", + node: { + __typename: "OrderEvent", + amount: null, + composedId: null, + date: "2018-10-26T09:38:02.467443+00:00", + email: null, + emailType: null, + id: "T3JkZXJFdmVudDo4NA==", + message: null, + orderNumber: "15", + oversoldItems: null, + quantity: null, + type: OrderEventsEnum.ORDER_FULLY_PAID, + user: null, }, - ], - }, - ordersToCapture: { - __typename: "OrderCountableConnection", - totalCount: 0, - }, - ordersToFulfill: { - __typename: "OrderCountableConnection", - totalCount: 1, - }, - ordersToday: { - __typename: "OrderCountableConnection", - totalCount: 1, - }, - productTopToday: { - __typename: "ProductVariantCountableConnection", - edges: [ - { - __typename: "ProductVariantCountableEdge", - node: { - __typename: "ProductVariant", - attributes: [ - { - __typename: "SelectedAttribute", - values: [ - { - __typename: "AttributeValue", - id: "QXR0cmlidXRlVmFsdWU6OTI=", - name: "XL", - sortOrder: 0, - }, - ], - }, - ], - id: "UHJvZHVjdFZhcmlhbnQ6NDM=", - product: { - __typename: "Product", - id: "UHJvZHVjdDo4", - name: "Black Hoodie", - thumbnail: { - __typename: "Image", - url: placeholderImage, - }, + }, + ], +}; + +export const topProducts: ( + placeholderImage: string, +) => HomeTopProductsQuery["productTopToday"] = (placeholderImage: string) => ({ + __typename: "ProductVariantCountableConnection", + edges: [ + { + __typename: "ProductVariantCountableEdge", + node: { + __typename: "ProductVariant", + attributes: [ + { + __typename: "SelectedAttribute", + values: [ + { + __typename: "AttributeValue", + id: "QXR0cmlidXRlVmFsdWU6OTI=", + name: "XL", + sortOrder: 0, + }, + ], + }, + ], + id: "UHJvZHVjdFZhcmlhbnQ6NDM=", + product: { + __typename: "Product", + id: "UHJvZHVjdDo4", + name: "Black Hoodie", + thumbnail: { + __typename: "Image", + url: placeholderImage, }, - quantityOrdered: 1, - revenue: { - __typename: "TaxedMoney", - gross: { - __typename: "Money", - amount: 37.65, - currency: "USD", - }, + }, + quantityOrdered: 1, + revenue: { + __typename: "TaxedMoney", + gross: { + __typename: "Money", + amount: 37.65, + currency: "USD", }, }, }, - { - __typename: "ProductVariantCountableEdge", - node: { - __typename: "ProductVariant", - attributes: [ - { - __typename: "SelectedAttribute", - values: [ - { - __typename: "AttributeValue", - id: "QXR0cmlidXRlVmFsdWU6OTI2=", - name: "2l", - sortOrder: 0, - }, - ], - }, - ], - id: "UHJvZHVjdFZhcmlhbnQ6NDM=2", - product: { - __typename: "Product", - id: "UHJvZHVjdDo4", - name: "Bean Juice", - thumbnail: { - __typename: "Image", - url: placeholderImage, - }, + }, + { + __typename: "ProductVariantCountableEdge", + node: { + __typename: "ProductVariant", + attributes: [ + { + __typename: "SelectedAttribute", + values: [ + { + __typename: "AttributeValue", + id: "QXR0cmlidXRlVmFsdWU6OTI2=", + name: "2l", + sortOrder: 0, + }, + ], + }, + ], + id: "UHJvZHVjdFZhcmlhbnQ6NDM=2", + product: { + __typename: "Product", + id: "UHJvZHVjdDo4", + name: "Bean Juice", + thumbnail: { + __typename: "Image", + url: placeholderImage, }, - quantityOrdered: 1, - revenue: { - __typename: "TaxedMoney", - gross: { - __typename: "Money", - amount: 37.65, - currency: "USD", - }, + }, + quantityOrdered: 1, + revenue: { + __typename: "TaxedMoney", + gross: { + __typename: "Money", + amount: 37.65, + currency: "USD", }, }, }, - { - __typename: "ProductVariantCountableEdge", - node: { - __typename: "ProductVariant", - attributes: [ - { - __typename: "SelectedAttribute", - values: [ - { - __typename: "AttributeValue", - id: "QXR0cmlidXRlVmFsdWU6OTI=3", - name: "L", - sortOrder: 0, - }, - ], - }, - ], - id: "UHJvZHVjdFZhcmlhbnQ6NDM=3", - product: { - __typename: "Product", - id: "UHJvZHVjdDo4", - name: "Black Hoodie", - thumbnail: { - __typename: "Image", - url: placeholderImage, - }, + }, + { + __typename: "ProductVariantCountableEdge", + node: { + __typename: "ProductVariant", + attributes: [ + { + __typename: "SelectedAttribute", + values: [ + { + __typename: "AttributeValue", + id: "QXR0cmlidXRlVmFsdWU6OTI=3", + name: "L", + sortOrder: 0, + }, + ], + }, + ], + id: "UHJvZHVjdFZhcmlhbnQ6NDM=3", + product: { + __typename: "Product", + id: "UHJvZHVjdDo4", + name: "Black Hoodie", + thumbnail: { + __typename: "Image", + url: placeholderImage, }, - quantityOrdered: 1, - revenue: { - __typename: "TaxedMoney", - gross: { - __typename: "Money", - amount: 37.65, - currency: "USD", - }, + }, + quantityOrdered: 1, + revenue: { + __typename: "TaxedMoney", + gross: { + __typename: "Money", + amount: 37.65, + currency: "USD", }, }, }, - ], - }, - productsOutOfStock: { - __typename: "ProductCountableConnection", - totalCount: 0, - }, + }, + ], +}); + +export const analitics: HomeAnaliticsQuery = { + __typename: "Query", salesToday: { __typename: "TaxedMoney", gross: { @@ -433,4 +442,8 @@ export const shop: (placeholderImage: string) => HomeQuery = ( currency: "USD", }, }, -}); + ordersToday: { + __typename: "OrderCountableConnection", + totalCount: 1, + }, +}; diff --git a/src/home/queries.ts b/src/home/queries.ts index de097bfa358..b831fc0226e 100644 --- a/src/home/queries.ts +++ b/src/home/queries.ts @@ -1,10 +1,9 @@ import { gql } from "@apollo/client"; -export const home = gql` - query Home( +export const homeAnalitics = gql` + query HomeAnalitics( $channel: String! $datePeriod: DateRangeInput! - $hasPermissionToManageProducts: Boolean! $hasPermissionToManageOrders: Boolean! ) { salesToday: ordersTotal(period: TODAY, channel: $channel) @@ -18,24 +17,41 @@ export const home = gql` @include(if: $hasPermissionToManageOrders) { totalCount } - ordersToFulfill: orders( - filter: { status: READY_TO_FULFILL } - channel: $channel - ) @include(if: $hasPermissionToManageOrders) { - totalCount - } - ordersToCapture: orders( - filter: { status: READY_TO_CAPTURE } - channel: $channel - ) @include(if: $hasPermissionToManageOrders) { - totalCount - } - productsOutOfStock: products( - filter: { stockAvailability: OUT_OF_STOCK } - channel: $channel - ) { - totalCount + } +`; + +export const homeActivities = gql` + query HomeActivities($hasPermissionToManageOrders: Boolean!) { + activities: homepageEvents(last: 10) + @include(if: $hasPermissionToManageOrders) { + edges { + node { + amount + composedId + date + email + emailType + id + message + orderNumber + oversoldItems + quantity + type + user { + id + email + } + } + } } + } +`; + +export const homeTopProducts = gql` + query HomeTopProducts( + $channel: String! + $hasPermissionToManageProducts: Boolean! + ) { productTopToday: reportProductSales( period: TODAY first: 5 @@ -67,27 +83,31 @@ export const home = gql` } } } - activities: homepageEvents(last: 10) - @include(if: $hasPermissionToManageOrders) { - edges { - node { - amount - composedId - date - email - emailType - id - message - orderNumber - oversoldItems - quantity - type - user { - id - email - } - } - } + } +`; + +export const homeNotifications = gql` + query homeNotifications( + $channel: String! + $hasPermissionToManageOrders: Boolean! + ) { + ordersToFulfill: orders( + filter: { status: READY_TO_FULFILL } + channel: $channel + ) @include(if: $hasPermissionToManageOrders) { + totalCount + } + ordersToCapture: orders( + filter: { status: READY_TO_CAPTURE } + channel: $channel + ) @include(if: $hasPermissionToManageOrders) { + totalCount + } + productsOutOfStock: products( + filter: { stockAvailability: OUT_OF_STOCK } + channel: $channel + ) { + totalCount } } `; diff --git a/src/home/types.ts b/src/home/types.ts index aca07b69469..56293d0df02 100644 --- a/src/home/types.ts +++ b/src/home/types.ts @@ -1,7 +1,30 @@ -import { HomeQuery } from "@dashboard/graphql"; +import { + HomeActivitiesQuery, + HomeAnaliticsQuery, + HomeTopProductsQuery, +} from "@dashboard/graphql"; import { RelayToFlat } from "@dashboard/types"; -export type Activities = RelayToFlat>; +export type Activities = RelayToFlat< + NonNullable +>; export type ProductTopToday = RelayToFlat< - NonNullable + NonNullable >; + +export interface Analitics { + orders: number | null; + sales: NonNullable["gross"]; +} + +export interface Notifications { + ordersToCapture: number | null; + ordersToFulfill: number | null; + productsOutOfStock: number; +} + +export interface HomeData { + data: T; + loading: boolean; + hasError: boolean; +} diff --git a/src/home/views/index.tsx b/src/home/views/index.tsx index 30e815a8b27..4d6e11b52ce 100644 --- a/src/home/views/index.tsx +++ b/src/home/views/index.tsx @@ -7,7 +7,10 @@ import { OrderStatusFilter, PermissionEnum, StockAvailability, - useHomeQuery, + useHomeActivitiesQuery, + useHomeAnaliticsQuery, + useHomeNotificationsQuery, + useHomeTopProductsQuery, } from "@dashboard/graphql"; import { mapEdgesToItems } from "@dashboard/utils/maps"; import React from "react"; @@ -24,28 +27,91 @@ const HomeSection = () => { const noChannel = !channel && typeof channel !== "undefined"; const userPermissions = user?.userPermissions || []; + const hasPermissionToManageOrders = hasPermissions(userPermissions, [ + PermissionEnum.MANAGE_ORDERS, + ]); + const hasPermissionToManageProducts = hasPermissions(userPermissions, [ + PermissionEnum.MANAGE_PRODUCTS, + ]); - const { data } = useHomeQuery({ - displayLoader: true, + const { + data: homeActivities, + loading: homeActivitiesLoading, + error: homeActivitiesError, + } = useHomeActivitiesQuery({ + skip: noChannel, + variables: { + hasPermissionToManageOrders, + }, + }); + + const { + data: homeTopProducts, + loading: homeTopProductsLoading, + error: homeTopProductsError, + } = useHomeTopProductsQuery({ + skip: noChannel, + variables: { + channel: channel?.slug, + hasPermissionToManageProducts, + }, + }); + + const { + data: homeNotificationsData, + loading: homeNotificationsLoaing, + error: homeNotificationsError, + } = useHomeNotificationsQuery({ + skip: noChannel, + variables: { + channel: channel?.slug, + hasPermissionToManageOrders, + }, + }); + + const { + data: homeAnaliticsData, + loading: homeAnaliticsLoading, + error: homeAnaliticsError, + } = useHomeAnaliticsQuery({ skip: noChannel, variables: { channel: channel?.slug, datePeriod: getDatePeriod(1), - hasPermissionToManageOrders: hasPermissions(userPermissions, [ - PermissionEnum.MANAGE_ORDERS, - ]), - hasPermissionToManageProducts: hasPermissions(userPermissions, [ - PermissionEnum.MANAGE_PRODUCTS, - ]), + hasPermissionToManageOrders, }, }); return ( { stockStatus: StockAvailability.OUT_OF_STOCK, channel: channel?.slug, })} - ordersToCapture={data?.ordersToCapture?.totalCount} - ordersToFulfill={data?.ordersToFulfill?.totalCount} - productsOutOfStock={data?.productsOutOfStock.totalCount} userName={getUserName(user, true)} noChannel={noChannel} /> diff --git a/src/hooks/useForm.ts b/src/hooks/useForm.ts index 9f11c5a7ba2..758517fb0ca 100644 --- a/src/hooks/useForm.ts +++ b/src/hooks/useForm.ts @@ -43,7 +43,7 @@ export interface UseFormResult Pick { reset: () => void; set: (data: Partial) => void; - triggerChange: () => void; + triggerChange: (value?: boolean) => void; handleChange: FormChange; toggleValue: FormChange; toggleValues: FormChange; diff --git a/src/index.html b/src/index.html index c2d85da78bc..06dd472df85 100644 --- a/src/index.html +++ b/src/index.html @@ -23,6 +23,7 @@ APPS_MARKETPLACE_API_URI: "<%= APPS_MARKETPLACE_API_URI %>", APPS_TUNNEL_URL_KEYWORDS: "<%= APPS_TUNNEL_URL_KEYWORDS %>", IS_CLOUD_INSTANCE: "<%= IS_CLOUD_INSTANCE %>", + LOCALE_CODE: "<%= LOCALE_CODE %>", }; diff --git a/src/intl.ts b/src/intl.ts index 31999f2732d..88c927b3955 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -247,6 +247,14 @@ export const errorMessages = defineMessages({ id: "7+GBlj", defaultMessage: "Error code {errorCode} {fieldError}", }, + voucherCodesErrorMessage: { + id: "2dgbGR", + defaultMessage: "Those codes already exist", + }, + voucherCodeErrorMessage: { + id: "WY3IXU", + defaultMessage: "This code already exists", + }, codeErrorFieldMessage: { id: "Qox+kb", defaultMessage: "on field {fieldName}", diff --git a/src/misc.ts b/src/misc.ts index f5390185fc7..1b45fb01122 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -311,12 +311,26 @@ export const parseLogMessage = ({ intl, code, field, + voucherCodes, }: { intl: IntlShape; code: string; field?: string; -}) => - intl.formatMessage(errorMessages.baseCodeErrorMessage, { + voucherCodes?: string[]; +}) => { + if (voucherCodes) { + return ( + intl.formatMessage( + voucherCodes.length > 1 + ? errorMessages.voucherCodesErrorMessage + : errorMessages.voucherCodeErrorMessage, + ) + + ": \n" + + voucherCodes.join("\n") + ); + } + + return intl.formatMessage(errorMessages.baseCodeErrorMessage, { errorCode: code, fieldError: field && @@ -324,6 +338,7 @@ export const parseLogMessage = ({ fieldName: field, }), }); +}; interface User { email: string; diff --git a/src/navigation/components/MenuItems/MenuItems.tsx b/src/navigation/components/MenuItems/MenuItems.tsx index b8d5440ed75..95aa92f1ab8 100644 --- a/src/navigation/components/MenuItems/MenuItems.tsx +++ b/src/navigation/components/MenuItems/MenuItems.tsx @@ -1,35 +1,16 @@ // @ts-strict-ignore -import CardTitle from "@dashboard/components/CardTitle"; +import { DashboardCard } from "@dashboard/components/Card"; import Skeleton from "@dashboard/components/Skeleton"; import { buttonMessages } from "@dashboard/intl"; import { RecursiveMenuItem } from "@dashboard/navigation/types"; -import { Card, CardActions, Paper, Typography } from "@material-ui/core"; -import EditIcon from "@material-ui/icons/Edit"; -import { - Button, - DeleteIcon, - IconButton, - makeStyles, - useTheme, -} from "@saleor/macaw-ui"; -import { GripIcon, vars } from "@saleor/macaw-ui-next"; -import clsx from "clsx"; -import React from "react"; +import { Box, Button } from "@saleor/macaw-ui-next"; +import React, { useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import SortableTree, { NodeRendererProps } from "react-sortable-tree"; import { MenuItemType } from "../MenuItemDialog"; -import { - getDiff, - getNodeData, - getNodeQuantity, - MenuTreeItem, - TreeItemProps, - TreeOperation, -} from "./tree"; - -const NODE_HEIGHT = 56; -const NODE_MARGIN = 40; +import { MenuItemsSortableTree } from "../MenuItemsSortableTree"; +import { getNodeData } from "../MenuItemsSortableTree/utils"; +import { getDiff, TreeOperation } from "./tree"; export interface MenuItemsProps { canUndo: boolean; @@ -41,175 +22,6 @@ export interface MenuItemsProps { onUndo: () => void; } -const useStyles = makeStyles( - theme => ({ - actions: { - "&&": { - padding: theme.spacing(2, 4), - }, - flexDirection: "row", - }, - container: { - background: theme.palette.grey[200], - }, - darkContainer: { - background: `${theme.palette.grey[800]} !important`, - }, - deleteButton: { - marginRight: theme.spacing(1), - }, - dragIcon: { - cursor: "grab", - }, - nodeTitle: { - cursor: "pointer", - marginLeft: theme.spacing(7), - }, - nodeActions: { - display: "flex", - gap: theme.spacing(1), - }, - root: { - "& .rst__collapseButton": { - display: "none", - }, - "& .rst__node": { - "&:first-of-type": { - "& $row": { - borderTop: `1px ${vars.colors.border.neutralPlain} solid`, - }, - }, - }, - }, - row: { - alignItems: "center", - background: vars.colors.background.surfaceNeutralPlain, - borderBottom: `1px ${vars.colors.border.neutralPlain} solid`, - borderRadius: 0, - display: "flex", - flexDirection: "row", - height: NODE_HEIGHT, - justifyContent: "flex-start", - paddingLeft: theme.spacing(3), - }, - rowContainer: { - "& > *": { - opacity: 1, - transition: `opacity ${theme.transitions.duration.standard}ms`, - }, - transition: `margin ${theme.transitions.duration.standard}ms`, - }, - rowContainerDragged: { - "&$rowContainer": { - "&:before": { - background: vars.colors.background.surfaceNeutralPlain, - border: `1px solid ${vars.colors.border.neutralPlain}`, - borderRadius: "100%", - content: "''", - height: 7, - left: 0, - position: "absolute", - top: -3, - width: 7, - }, - borderTop: `1px solid ${vars.colors.border.neutralPlain}`, - height: 0, - position: "relative", - top: -1, - }, - }, - rowContainerPlaceholder: { - opacity: 0, - }, - spacer: { - flex: 1, - }, - }), - { name: "MenuItems" }, -); - -const Placeholder: React.FC = props => { - const classes = useStyles(props); - - return ( - - - - - - ); -}; - -const Node: React.FC> = props => { - const { node, path, connectDragPreview, connectDragSource, isDragging } = - props; - const classes = useStyles(props); - - const draggedClassName = clsx( - classes.rowContainer, - classes.rowContainerDragged, - ); - const defaultClassName = isDragging ? draggedClassName : classes.rowContainer; - const placeholderClassName = clsx( - classes.rowContainer, - classes.rowContainerPlaceholder, - ); - - const [className, setClassName] = React.useState(defaultClassName); - React.useEffect(() => setClassName(defaultClassName), [isDragging]); - - const handleDragStart = () => { - setClassName(placeholderClassName); - setTimeout(() => setClassName(defaultClassName), 0); - }; - - return connectDragPreview( -
- - {connectDragSource( -
- -
, - )} - - {node.title} - -
-
- - - - - - node.onChange([ - { - id: node.id, - type: "remove", - }, - ]) - } - > - - -
- -
, - ); -}; - const MenuItems: React.FC = props => { const { canUndo, @@ -220,79 +32,53 @@ const MenuItems: React.FC = props => { onItemEdit, onUndo, } = props; - const classes = useStyles(props); - const intl = useIntl(); - const { themeType } = useTheme(); + const currentTree = useMemo(() => items.map(getNodeData), [items]); return ( - - + + + {intl.formatMessage({ + id: "dEUZg2", + defaultMessage: "Menu Items", + description: "header", + })} - } - /> -
- {items === undefined ? ( - - ) : ( - ({ - className: classes.row, - style: { - marginLeft: NODE_MARGIN * (path.length - 1), - }, - })} - maxDepth={5} - isVirtualized={false} - rowHeight={NODE_HEIGHT} - treeData={items.map(item => - getNodeData(item, onChange, onItemClick, onItemEdit), - )} - theme={{ - nodeContentRenderer: Node, - }} - onChange={newTree => - onChange( - getDiff( - items.map(item => - getNodeData(item, onChange, onItemClick, onItemEdit), - ), - newTree as MenuTreeItem[], - ), - ) - } - placeholderRenderer={Placeholder} - /> - )} -
- - - -
+ + ); }; MenuItems.displayName = "MenuItems"; diff --git a/src/navigation/components/MenuItems/__snapshots__/tree.test.ts.snap b/src/navigation/components/MenuItems/__snapshots__/tree.test.ts.snap index 62966e233da..86bff773df6 100644 --- a/src/navigation/components/MenuItems/__snapshots__/tree.test.ts.snap +++ b/src/navigation/components/MenuItems/__snapshots__/tree.test.ts.snap @@ -1,34 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Properly computes diffs # 1`] = ` +exports[`MenuItems tree - getDiff should return array with operations 1`] = ` Array [ Object { - "id": "1glasses", - "parentId": "0jewelry", - "sortOrder": 0, - "type": "move", - }, -] -`; - -exports[`Properly computes diffs # 2`] = ` -Array [ - Object { - "id": "1glasses", + "id": "4apparel", "parentId": "2accessories", - "sortOrder": -1, + "sortOrder": 1, "type": "move", }, -] -`; - -exports[`Properly computes diffs # 3`] = ` -Array [ Object { - "id": "2accessories", - "parentId": "4apparel", + "id": "0jewelry", + "parentId": "3groceries", "sortOrder": 0, "type": "move", }, ] `; + +exports[`MenuItems tree - getDiff should return orinal tree when no changes 1`] = `Array []`; diff --git a/src/navigation/components/MenuItems/tree.test.ts b/src/navigation/components/MenuItems/tree.test.ts index 88e3abcd6b9..194de31aa9e 100644 --- a/src/navigation/components/MenuItems/tree.test.ts +++ b/src/navigation/components/MenuItems/tree.test.ts @@ -1,88 +1,74 @@ // @ts-strict-ignore -import { - addNodeUnderParent, - find, - insertNode, - removeNode, -} from "react-sortable-tree"; +import { MenuItemFragment } from "@dashboard/graphql"; +import { MenuTreeItem } from "@dashboard/navigation/types"; -import { getDiff, MenuTreeItem } from "./tree"; +import { getDiff } from "./tree"; const originalTree: MenuTreeItem[] = [ { children: [ - { children: [], expanded: true, id: "0jewelry", title: "Jewelry" }, - { children: [], expanded: true, id: "1glasses", title: "Glasses" }, + { + children: [], + id: "0jewelry", + data: { name: "Jewelry" } as MenuItemFragment, + }, + { + children: [], + id: "1glasses", + data: { name: "Glasses" } as MenuItemFragment, + }, ], - expanded: true, id: "2accessories", - title: "Accessories", + data: { name: "Accessories" } as MenuItemFragment, + }, + { + children: [], + id: "3groceries", + data: { name: "Groceries" } as MenuItemFragment, + }, + { + children: [], + id: "4apparel", + data: { name: "Apparel" } as MenuItemFragment, }, - { children: [], expanded: true, id: "3groceries", title: "Groceries" }, - { children: [], expanded: true, id: "4apparel", title: "Apparel" }, ]; -function getNodeKey(node: any) { - return node.treeIndex; -} - -function moveNode( - tree: MenuTreeItem[], - src: string, - target: string, - asChild: boolean, -) { - const { matches: srcNodeCandidates } = find({ - getNodeKey, - searchMethod: ({ node }) => node.id === src, - treeData: tree, +describe("MenuItems tree - getDiff", () => { + it("should return orinal tree when no changes", () => { + const diff = getDiff(originalTree, []); + expect(diff).toMatchSnapshot(); }); - const srcNodeData = srcNodeCandidates[0]; - const treeAfterRemoval = removeNode({ - getNodeKey, - path: srcNodeData.path, - treeData: tree, - }).treeData; - - const { matches: targetNodeCandidates } = find({ - getNodeKey, - searchMethod: ({ node }) => node.id === target, - treeData: treeAfterRemoval, + it("should return array with operations", () => { + const diff = getDiff(originalTree, [ + { + children: [ + { + children: [], + id: "1glasses", + data: { name: "Glasses" } as MenuItemFragment, + }, + { + children: [], + id: "4apparel", + data: { name: "Apparel" } as MenuItemFragment, + }, + ], + id: "2accessories", + data: { name: "Accessories" } as MenuItemFragment, + }, + { + children: [ + { + children: [], + id: "0jewelry", + data: { name: "Jewelry" } as MenuItemFragment, + }, + ], + id: "3groceries", + data: { name: "Groceries" } as MenuItemFragment, + }, + ]); + expect(diff).toMatchSnapshot(); }); - const targetNodeData = targetNodeCandidates[0]; - - const treeAfterInsertion = asChild - ? addNodeUnderParent({ - addAsFirstChild: true, - getNodeKey, - ignoreCollapsed: false, - newNode: srcNodeData.node, - parentKey: targetNodeData.treeIndex, - treeData: treeAfterRemoval, - }).treeData - : insertNode({ - depth: targetNodeData.path.length, - getNodeKey, - minimumTreeIndex: targetNodeData.treeIndex, - newNode: srcNodeData.node, - treeData: treeAfterRemoval, - }).treeData; - - return treeAfterInsertion as MenuTreeItem[]; -} - -describe("Properly computes diffs", () => { - const testTable = [ - moveNode(originalTree, "1glasses", "0jewelry", true), - moveNode(originalTree, "1glasses", "0jewelry", false), - moveNode(originalTree, "2accessories", "4apparel", true), - ]; - - testTable.forEach(testData => - it("#", () => { - const diff = getDiff(originalTree, testData); - expect(diff).toMatchSnapshot(); - }), - ); }); diff --git a/src/navigation/components/MenuItems/tree.ts b/src/navigation/components/MenuItems/tree.ts index bc7cc01a8c1..08629ffa109 100644 --- a/src/navigation/components/MenuItems/tree.ts +++ b/src/navigation/components/MenuItems/tree.ts @@ -1,9 +1,7 @@ // @ts-strict-ignore -import { RecursiveMenuItem } from "@dashboard/navigation/types"; -import { getPatch } from "fast-array-diff"; -import { TreeItem } from "react-sortable-tree"; -import { MenuItemType } from "../MenuItemDialog"; +import { MenuTreeItem } from "@dashboard/navigation/types"; +import { getPatch } from "fast-array-diff"; export type TreeOperationType = "move" | "remove"; export interface TreeOperation { @@ -13,66 +11,6 @@ export interface TreeOperation { sortOrder?: number; simulatedMove?: boolean; } -export interface TreeItemProps { - id: string; - onChange?: (operations: TreeOperation[]) => void; - onClick?: () => void; - onEdit?: () => void; -} - -export type MenuTreeItem = TreeItem; - -export const unknownTypeError = Error("Unknown type"); - -function treeToMap( - tree: MenuTreeItem[], - parent: string, -): Record { - const childrenList = tree.map(node => node.id); - const childrenMaps = tree.map(node => ({ - id: node.id, - mappedNodes: treeToMap(node.children as MenuTreeItem[], node.id), - })); - - return { - [parent]: childrenList, - ...childrenMaps.reduce( - (acc, childMap) => ({ - ...acc, - ...childMap.mappedNodes, - }), - {}, - ), - }; -} - -export function getItemType(item: RecursiveMenuItem): MenuItemType { - if (item.category) { - return "category"; - } else if (item.collection) { - return "collection"; - } else if (item.page) { - return "page"; - } else if (item.url) { - return "link"; - } else { - throw unknownTypeError; - } -} - -export function getItemId(item: RecursiveMenuItem): string { - if (item.category) { - return item.category.id; - } else if (item.collection) { - return item.collection.id; - } else if (item.page) { - return item.page.id; - } else if (item.url) { - return item.url; - } else { - throw unknownTypeError; - } -} export function getDiff( originalTree: MenuTreeItem[], @@ -137,28 +75,24 @@ export function getDiff( return diff.filter(d => !!d); } -export function getNodeData( - item: RecursiveMenuItem, - onChange: (operations: TreeOperation[]) => void, - onClick: (id: string, type: MenuItemType) => void, - onEdit: (id: string) => void, -): MenuTreeItem { +function treeToMap( + tree: MenuTreeItem[], + parent: string, +): Record { + const childrenList = tree.map(node => node.id); + const childrenMaps = tree.map(node => ({ + id: node.id, + mappedNodes: treeToMap(node.children as MenuTreeItem[], node.id.toString()), + })); + return { - children: item.children.map(child => - getNodeData(child, onChange, onClick, onEdit), + [parent]: childrenList, + ...childrenMaps.reduce( + (acc, childMap) => ({ + ...acc, + ...childMap.mappedNodes, + }), + {}, ), - expanded: true, - id: item.id, - onChange, - onClick: () => onClick(getItemId(item), getItemType(item)), - onEdit: () => onEdit(item.id), - title: item.name, }; } - -export function getNodeQuantity(items: RecursiveMenuItem[]): number { - return items.reduce( - (acc, curr) => acc + getNodeQuantity(curr.children), - items.length, - ); -} diff --git a/src/navigation/components/MenuItemsSortableTree/MenuItemsSortableTree.tsx b/src/navigation/components/MenuItemsSortableTree/MenuItemsSortableTree.tsx new file mode 100644 index 00000000000..915d4f4a7a8 --- /dev/null +++ b/src/navigation/components/MenuItemsSortableTree/MenuItemsSortableTree.tsx @@ -0,0 +1,60 @@ +import { SortableTree } from "@dashboard/components/SortableTree"; +import { MenuTreeItem, RecursiveMenuItem } from "@dashboard/navigation/types"; +import { UniqueIdentifier } from "@dnd-kit/core"; +import { Box, Text } from "@saleor/macaw-ui-next"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { MenuItemType } from "../MenuItemDialog"; +import { MenuItemsSortableTreeItem } from "./MenuItemsSortableTreeItem"; +import { getNodeData } from "./utils"; + +interface MenuItemsSortableTreeProps { + items: RecursiveMenuItem[]; + onChange: (newTree: MenuTreeItem[]) => void; + onItemClick: (id: UniqueIdentifier, type: MenuItemType) => void; + onItemEdit: (id: UniqueIdentifier) => void; + onItemRemove: (id: UniqueIdentifier) => void; +} + +export const MenuItemsSortableTree = ({ + items, + onItemClick, + onItemEdit, + onItemRemove, + onChange, +}: MenuItemsSortableTreeProps) => { + if (!items.length) { + return ( + + + + + + ); + } + + return ( + ( + + )} + /> + ); +}; diff --git a/src/navigation/components/MenuItemsSortableTree/MenuItemsSortableTreeItem.tsx b/src/navigation/components/MenuItemsSortableTree/MenuItemsSortableTreeItem.tsx new file mode 100644 index 00000000000..3a01e4aa594 --- /dev/null +++ b/src/navigation/components/MenuItemsSortableTree/MenuItemsSortableTreeItem.tsx @@ -0,0 +1,112 @@ +import { TreeItemComponentProps } from "@dashboard/components/SortableTree/types"; +import { buttonMessages } from "@dashboard/intl"; +import { MenuItemType } from "@dashboard/navigation/components/MenuItemDialog"; +import { RecursiveMenuItem } from "@dashboard/navigation/types"; +import { UniqueIdentifier } from "@dnd-kit/core"; +import { + Box, + Button, + EditIcon, + GripIcon, + Text, + TrashBinIcon, +} from "@saleor/macaw-ui-next"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { getItemId, getItemType } from "./utils"; + +interface TreeItemProps extends TreeItemComponentProps { + onClick: (id: UniqueIdentifier, menuItemType: MenuItemType) => void; + onEdit: (id: UniqueIdentifier) => void; + onRemove: (id: UniqueIdentifier) => void; +} + +export const MenuItemsSortableTreeItem = ({ + innerRef, + id, + data, + childCount, + clone, + depth, + disableInteraction, + ghost, + handleProps, + indentationWidth, + style, + wrapperRef, + onEdit, + onClick, + onRemove, +}: TreeItemProps) => { + return ( + + + + + {data.name} + + + + + + + {clone && childCount && childCount > 1 ? ( + + {childCount} + + ) : null} + + + ); +}; diff --git a/src/navigation/components/MenuItemsSortableTree/index.ts b/src/navigation/components/MenuItemsSortableTree/index.ts new file mode 100644 index 00000000000..77b399eaec0 --- /dev/null +++ b/src/navigation/components/MenuItemsSortableTree/index.ts @@ -0,0 +1 @@ +export * from "./MenuItemsSortableTree"; diff --git a/src/navigation/components/MenuItemsSortableTree/utils.ts b/src/navigation/components/MenuItemsSortableTree/utils.ts new file mode 100644 index 00000000000..18ba2314067 --- /dev/null +++ b/src/navigation/components/MenuItemsSortableTree/utils.ts @@ -0,0 +1,41 @@ +import { MenuTreeItem, RecursiveMenuItem } from "@dashboard/navigation/types"; + +import { MenuItemType } from "../MenuItemDialog"; + +export function getNodeData(item: RecursiveMenuItem): MenuTreeItem { + return { + children: item.children?.map(child => getNodeData(child)) ?? [], + data: item, + id: item.id, + }; +} + +export const unknownTypeError = Error("Unknown type"); + +export function getItemType(item: RecursiveMenuItem): MenuItemType { + if (item.category) { + return "category"; + } else if (item.collection) { + return "collection"; + } else if (item.page) { + return "page"; + } else if (item.url) { + return "link"; + } else { + throw unknownTypeError; + } +} + +export function getItemId(item: RecursiveMenuItem): string { + if (item.category) { + return item.category.id; + } else if (item.collection) { + return item.collection.id; + } else if (item.page) { + return item.page.id; + } else if (item.url) { + return item.url; + } else { + throw unknownTypeError; + } +} diff --git a/src/navigation/types.ts b/src/navigation/types.ts index affa150c229..70f9976e24a 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -1,5 +1,8 @@ +import { TreeItem } from "@dashboard/components/SortableTree/types"; import { MenuItemFragment } from "@dashboard/graphql"; export type RecursiveMenuItem = MenuItemFragment & { children?: RecursiveMenuItem[]; }; + +export type MenuTreeItem = TreeItem; diff --git a/src/navigation/views/MenuDetails/index.tsx b/src/navigation/views/MenuDetails/index.tsx index 45f9222a3fb..46514ab896e 100644 --- a/src/navigation/views/MenuDetails/index.tsx +++ b/src/navigation/views/MenuDetails/index.tsx @@ -34,7 +34,7 @@ import { getItemId, getItemType, unknownTypeError, -} from "../../components/MenuItems"; +} from "../../components/MenuItemsSortableTree/utils"; import { menuUrl, MenuUrlQueryParams } from "../../urls"; import { handleDelete, diff --git a/src/navigation/views/MenuDetails/utils.ts b/src/navigation/views/MenuDetails/utils.ts index a1ca74dcae8..73ad386fee9 100644 --- a/src/navigation/views/MenuDetails/utils.ts +++ b/src/navigation/views/MenuDetails/utils.ts @@ -7,7 +7,7 @@ import { import { MenuDetailsSubmitData } from "../../components/MenuDetailsPage"; import { MenuItemDialogFormData } from "../../components/MenuItemDialog"; -import { unknownTypeError } from "../../components/MenuItems"; +import { unknownTypeError } from "../../components/MenuItemsSortableTree/utils"; export function getMenuItemInputData( data: MenuItemDialogFormData, diff --git a/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.stories.tsx b/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.stories.tsx index ed26745e0d0..64007e790c3 100644 --- a/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.stories.tsx +++ b/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.stories.tsx @@ -220,6 +220,7 @@ const props: OrderGrantRefundPageProps = { __typename: "Fulfillment", }, ], + grantedRefunds: [], shippingPrice: { gross: { amount: 85.23, diff --git a/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.tsx b/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.tsx index 588d33801cc..068314550a0 100644 --- a/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.tsx +++ b/src/orders/components/OrderGrantRefundPage/OrderGrantRefundPage.tsx @@ -1,18 +1,25 @@ // @ts-strict-ignore import { TopNav } from "@dashboard/components/AppLayout/TopNav"; -import CardSpacer from "@dashboard/components/CardSpacer"; +import { DashboardCard } from "@dashboard/components/Card"; import { ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton"; import { DetailPageLayout } from "@dashboard/components/Layouts"; -import Skeleton from "@dashboard/components/Skeleton"; -import { OrderDetailsGrantRefundFragment } from "@dashboard/graphql"; +import { formatMoneyAmount } from "@dashboard/components/Money"; +import PriceField from "@dashboard/components/PriceField"; +import Savebar from "@dashboard/components/Savebar"; +import { + OrderDetailsGrantedRefundFragment, + OrderDetailsGrantRefundFragment, +} from "@dashboard/graphql"; +import useLocale from "@dashboard/hooks/useLocale"; +import useNavigator from "@dashboard/hooks/useNavigator"; import { orderUrl } from "@dashboard/orders/urls"; -import { Card, CardContent, TextField, Typography } from "@material-ui/core"; -import { Text } from "@saleor/macaw-ui-next"; -import React from "react"; +import { Box, Input, Skeleton, Text } from "@saleor/macaw-ui-next"; +import React, { useEffect, useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { getOrderTitleMessage } from "../OrderCardTitle/utils"; -import { ProductsCard, RefundCard } from "./components"; +import { ProductsCard } from "./components/ProductCard"; +import { ShippingIncluded } from "./components/ShippingInluded"; import { GrantRefundContext } from "./context"; import { OrderGrantRefundFormData, useGrantRefundForm } from "./form"; import { grantRefundPageMessages } from "./messages"; @@ -21,8 +28,14 @@ import { grantRefundDefaultState, grantRefundReducer, } from "./reducer"; -import { useStyles } from "./styles"; -import { calculateTotalPrice, getFulfilmentSubtitle } from "./utils"; +import { + calculateCanRefundShipping, + calculateTotalPrice, + getFulfilmentSubtitle, + getGrantedRefundData, + getRefundAmountValue, + prepareLineData, +} from "./utils"; export interface OrderGrantRefundPageProps { order: OrderDetailsGrantRefundFragment; @@ -30,7 +43,7 @@ export interface OrderGrantRefundPageProps { submitState: ConfirmButtonTransitionState; onSubmit: (data: OrderGrantRefundFormData) => void; isEdit?: boolean; - initialData?: OrderGrantRefundFormData; + initialData?: OrderDetailsGrantedRefundFragment; } const OrderGrantRefundPage: React.FC = ({ @@ -41,36 +54,68 @@ const OrderGrantRefundPage: React.FC = ({ isEdit, initialData, }) => { - const classes = useStyles(); const intl = useIntl(); + const { locale } = useLocale(); + const navigate = useNavigator(); + + const grantedRefund = useMemo( + () => getGrantedRefundData(initialData), + [initialData], + ); const unfulfilledLines = (order?.lines ?? []).filter( line => line.quantityToFulfill > 0, ); - const [state, dispatch] = React.useReducer( grantRefundReducer, grantRefundDefaultState, ); - React.useEffect(() => { + useEffect(() => { + if (grantedRefund) { + dispatch({ + type: "setRefundShipping", + refundShipping: grantedRefund.grantRefundForShipping, + }); + } + }, [grantedRefund]); + + useEffect(() => { if (order?.id) { dispatch({ type: "initState", - state: getGrantRefundReducerInitialState(order), + state: getGrantRefundReducerInitialState(order, initialData), }); } - }, [order]); + }, [order, initialData]); - const { set, change, data, submit, setIsDirty } = useGrantRefundForm({ - onSubmit, - initialData, - }); + const lines = prepareLineData(state.lines); + const canRefundShipping = calculateCanRefundShipping( + grantedRefund, + order?.grantedRefunds, + ); - const amount = parseFloat(data.amount); - const submitDisabled = Number.isNaN(amount) || amount <= 0; + const { set, change, data, submit, setIsDirty, isFormDirty } = + useGrantRefundForm({ + onSubmit, + grantedRefund, + lines, + // Send grantRefundForShipping only when it's different than the one + grantRefundForShipping: + grantedRefund?.grantRefundForShipping === state.refundShipping + ? undefined + : state.refundShipping, + }); const totalSelectedPrice = calculateTotalPrice(state, order); + const amountValue = getRefundAmountValue({ + isEditedRefundAmount: grantedRefund !== undefined, + isAmountInputDirty: isFormDirty.amount, + refundAmount: Number(data.amount), + totalCalulatedPrice: totalSelectedPrice, + }); + + const currency = order?.total?.gross?.currency ?? ""; const handleSubmit = (e: React.FormEvent) => { e.stopPropagation(); @@ -78,8 +123,22 @@ const OrderGrantRefundPage: React.FC = ({ submit(); }; + const getRefundAmountDisplayValue = () => { + if (isFormDirty) { + return amountValue.toString(); + } + + return formatMoneyAmount( + { + amount: amountValue, + currency, + }, + locale, + ); + }; + return ( - + = ({ /> } > -
+ { @@ -103,81 +162,108 @@ const OrderGrantRefundPage: React.FC = ({ }} > - - - + + + - - - -
- {loading && } - - } - lines={unfulfilledLines} - /> - {order?.fulfillments?.map?.(fulfillment => ( - - {getFulfilmentSubtitle(order, fulfillment)} - - } - lines={fulfillment.lines.map( - ({ orderLine, id, quantity }) => ({ - ...orderLine, - id, - quantity, - }), - )} + + {loading ? ( + + ) : ( + <> + + } + lines={unfulfilledLines} + /> + + {order?.fulfillments?.map?.(fulfillment => ( + + {getFulfilmentSubtitle(order, fulfillment)} + + } + lines={fulfillment.lines.map( + ({ orderLine, id, quantity }) => { + return { + ...orderLine, + id, + quantity, + }; + }, + )} + /> + ))} + + )} + + - ))} - - - + + + + - - -
+ + +
- - -
+ navigate(orderUrl(order?.id))} + onSubmit={submit} + state={submitState} + disabled={loading} + />
); }; diff --git a/src/orders/components/OrderGrantRefundPage/components/ProductCard.tsx b/src/orders/components/OrderGrantRefundPage/components/ProductCard.tsx index 19babbcf8aa..59677499e9c 100644 --- a/src/orders/components/OrderGrantRefundPage/components/ProductCard.tsx +++ b/src/orders/components/OrderGrantRefundPage/components/ProductCard.tsx @@ -1,18 +1,10 @@ // @ts-strict-ignore -import { Button } from "@dashboard/components/Button"; -import CardTitle from "@dashboard/components/CardTitle"; import TableCellAvatar from "@dashboard/components/TableCellAvatar"; import TableRowLink from "@dashboard/components/TableRowLink"; import { OrderLineGrantRefundFragment } from "@dashboard/graphql"; import { renderCollection } from "@dashboard/misc"; -import { - Card, - Table, - TableBody, - TableCell, - TableHead, - TextField, -} from "@material-ui/core"; +import { Table, TableBody, TableCell, TableHead } from "@material-ui/core"; +import { Box, Button, Input, Text } from "@saleor/macaw-ui-next"; import React from "react"; import { FormattedMessage } from "react-intl"; @@ -48,35 +40,36 @@ export const ProductsCard: React.FC = ({ type: "setQuantity", lineId: line.id, amount: value, + unitPrice: line.unitPrice.gross.amount, }); }; const handleSetMaxQuanity = () => { dispatch({ type: "setMaxQuantity", - lineIds: lines.map(line => line.id), + lines: lines.map(line => ({ + id: line.id, + quantity: state.lines.get(line.id)?.availableQuantity ?? 0, + unitPrice: line.unitPrice.gross.amount, + })), }); }; return ( - - - {title} - {subtitle} - - } - toolbar={ - - } - > + <> + + + {title} + {subtitle} + + + @@ -92,46 +85,49 @@ export const ProductsCard: React.FC = ({ {renderCollection( lines, - line => ( - - -
- {line?.productName} - - {line.variantName} - -
-
- - {line.quantity} - - - - / {line?.quantity} - - ), - }} - /> - -
- ), + line => { + const stateLine = state.lines.get(line.id); + + return ( + + +
+ {line?.productName} + {line.variantName} +
+
+ + {line.quantity} + + + + / {stateLine?.availableQuantity} + + ) + } + /> + +
+ ); + }, () => ( @@ -145,6 +141,6 @@ export const ProductsCard: React.FC = ({ )}
-
+ ); }; diff --git a/src/orders/components/OrderGrantRefundPage/components/RefundCard.tsx b/src/orders/components/OrderGrantRefundPage/components/RefundCard.tsx deleted file mode 100644 index 3729256e8fc..00000000000 --- a/src/orders/components/OrderGrantRefundPage/components/RefundCard.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import CardTitle from "@dashboard/components/CardTitle"; -import Checkbox from "@dashboard/components/Checkbox"; -import { - ConfirmButton, - ConfirmButtonTransitionState, -} from "@dashboard/components/ConfirmButton"; -import { formatMoneyAmount } from "@dashboard/components/Money"; -import PriceField from "@dashboard/components/PriceField"; -import Skeleton from "@dashboard/components/Skeleton"; -import { OrderDetailsGrantRefundFragment } from "@dashboard/graphql"; -import useLocale from "@dashboard/hooks/useLocale"; -import { buttonMessages } from "@dashboard/intl"; -import { Card, CardContent, Typography } from "@material-ui/core"; -import { useId } from "@reach/auto-id"; -import { Button } from "@saleor/macaw-ui-next"; -import React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; - -import { useGrantRefundContext } from "../context"; -import { OrderGrantRefundFormData } from "../form"; -import { grantRefundPageMessages } from "../messages"; -import { useRefundCardStyles } from "../styles"; - -interface RefundCardProps { - order: OrderDetailsGrantRefundFragment | null; - loading: boolean; - submitState: ConfirmButtonTransitionState; - isEdit: boolean; - submitDisabled: boolean; -} - -export const RefundCard = ({ - order, - loading, - submitState, - isEdit, - submitDisabled, -}: RefundCardProps) => { - const intl = useIntl(); - const { locale } = useLocale(); - const classes = useRefundCardStyles(); - const id = useId(); - - const { state, dispatch, form, totalSelectedPrice } = useGrantRefundContext(); - - const currency = order?.total?.gross?.currency ?? ""; - - return ( - - } - /> - - - - - {order ? ( -
- dispatch({ type: "toggleRefundShipping" })} - data-test-id="refundShippingCheckbox" - /> - -
- ) : ( -
- -
- )} - -
- - - - - {currency}{" "} - {formatMoneyAmount( - { - amount: totalSelectedPrice ?? 0, - currency, - }, - locale, - )} - - -
-
- -
-
- - {isEdit ? ( - - ) : ( - - )} - -
-
-
- ); -}; diff --git a/src/orders/components/OrderGrantRefundPage/components/ShippingInluded.tsx b/src/orders/components/OrderGrantRefundPage/components/ShippingInluded.tsx new file mode 100644 index 00000000000..58e058e761e --- /dev/null +++ b/src/orders/components/OrderGrantRefundPage/components/ShippingInluded.tsx @@ -0,0 +1,59 @@ +import { formatMoneyAmount } from "@dashboard/components/Money"; +import useLocale from "@dashboard/hooks/useLocale"; +import { IMoney } from "@dashboard/utils/intl"; +import { useId } from "@reach/auto-id"; +import { Box, Skeleton, Text, Toggle } from "@saleor/macaw-ui-next"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { useGrantRefundContext } from "../context"; +import { grantRefundPageMessages } from "../messages"; + +interface ShippingIncludedProps { + currency: string; + amount: IMoney; + canRefundShipping: boolean; +} + +export const ShippingIncluded = ({ + currency, + amount, + canRefundShipping, +}: ShippingIncludedProps) => { + const id = useId(); + const { locale } = useLocale(); + const { state, dispatch } = useGrantRefundContext(); + + return ( + + dispatch({ type: "toggleRefundShipping" })} + data-test-id="refundShippingCheckbox" + disabled={!currency || !canRefundShipping} + > + {!currency ? ( + + ) : ( + + )} + + + {!canRefundShipping && ( + + + + )} + + ); +}; diff --git a/src/orders/components/OrderGrantRefundPage/components/index.ts b/src/orders/components/OrderGrantRefundPage/components/index.ts deleted file mode 100644 index 10fb5c35502..00000000000 --- a/src/orders/components/OrderGrantRefundPage/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./ProductCard"; -export * from "./RefundCard"; diff --git a/src/orders/components/OrderGrantRefundPage/form.ts b/src/orders/components/OrderGrantRefundPage/form.ts index cbb7fd5cb8e..b56e5d9844d 100644 --- a/src/orders/components/OrderGrantRefundPage/form.ts +++ b/src/orders/components/OrderGrantRefundPage/form.ts @@ -1,29 +1,48 @@ import { useExitFormDialog } from "@dashboard/components/Form/useExitFormDialog"; -import useForm from "@dashboard/hooks/useForm"; +import { OrderGrantRefundCreateLineInput } from "@dashboard/graphql"; +import useForm, { FormChange } from "@dashboard/hooks/useForm"; import useHandleFormSubmit from "@dashboard/hooks/useHandleFormSubmit"; import React from "react"; export interface OrderGrantRefundFormData { - amount: string; + amount: number | undefined; reason: string; + lines: OrderGrantRefundCreateLineInput[]; + grantRefundForShipping: boolean; } const defaultInitialData: OrderGrantRefundFormData = { - amount: "", + amount: 0, reason: "", + lines: [], + grantRefundForShipping: false, }; +export interface Line { + id: string; + quantity: number; +} + interface GrantRefundFormHookProps { onSubmit: (data: OrderGrantRefundFormData) => void; - initialData?: OrderGrantRefundFormData; + grantedRefund?: OrderGrantRefundFormData; + lines: Line[]; + grantRefundForShipping: boolean; } export const useGrantRefundForm = ({ onSubmit, - initialData, + grantedRefund, + lines, + grantRefundForShipping, }: GrantRefundFormHookProps) => { + const [isFormDirty, setIsFormDirty] = React.useState({ + amount: false, + reason: false, + }); + const { set, change, data, formId } = useForm( - initialData ?? defaultInitialData, + grantedRefund ?? defaultInitialData, undefined, { confirmLeave: true, @@ -39,9 +58,46 @@ export const useGrantRefundForm = ({ onSubmit, }); - const submit = () => handleFormSubmit(data); + const getAmountValue = () => { + // When editing always return the amount value + if (grantedRefund) { + return data.amount; + } + + // When creating and user doesn not provide value, value will be calculated base on lines and shipping + if (!isFormDirty.amount) { + return undefined; + } + + // When creating and user provide value, return the provided value + return data.amount; + }; + + const submit = () => + handleFormSubmit({ + ...data, + amount: getAmountValue(), + lines, + grantRefundForShipping, + }); React.useEffect(() => setExitDialogSubmitRef(submit), [submit]); - return { set, change, data, submit, setIsDirty }; + const handleChange: FormChange = e => { + if (e.target.name === "amount") + setIsFormDirty({ ...isFormDirty, amount: true }); + if (e.target.name === "reason") + setIsFormDirty({ ...isFormDirty, reason: true }); + + change(e); + }; + + return { + set, + change: handleChange, + data, + submit, + setIsDirty, + isFormDirty, + }; }; diff --git a/src/orders/components/OrderGrantRefundPage/reducer.ts b/src/orders/components/OrderGrantRefundPage/reducer.ts index 82bf283542b..bf2c54abcb4 100644 --- a/src/orders/components/OrderGrantRefundPage/reducer.ts +++ b/src/orders/components/OrderGrantRefundPage/reducer.ts @@ -1,11 +1,18 @@ // @ts-strict-ignore -import { OrderDetailsGrantRefundFragment } from "@dashboard/graphql"; +import { + OrderDetailsGrantedRefundFragment, + OrderDetailsGrantRefundFragment, +} from "@dashboard/graphql"; import { exhaustiveCheck } from "@dashboard/utils/ts"; +import { getLineAvailableQuantity } from "./utils"; + export interface ReducerOrderLine { selectedQuantity: number; availableQuantity: number; + initialQuantity?: number; unitPrice: number; + isDirty: boolean; } export interface GrantRefundState { @@ -20,10 +27,15 @@ export type GrantRefundAction = type: "setQuantity"; lineId: string; amount: number; + unitPrice: number; } | { type: "setMaxQuantity"; - lineIds: string[]; + lines: Array<{ + id: string; + quantity: number; + unitPrice: number; + }>; } | { type: "initState"; @@ -31,36 +43,29 @@ export type GrantRefundAction = } | { type: "toggleRefundShipping"; + } + | { + type: "setRefundShipping"; + refundShipping: boolean; }; export const getGrantRefundReducerInitialState = ( order: OrderDetailsGrantRefundFragment, + grantedRefund?: OrderDetailsGrantedRefundFragment, ): GrantRefundState => { + const toGrantRefundLine = createToGrantRefundLineMap(order, grantedRefund); + const unfulfilledLines = order?.lines .filter(line => line.quantityToFulfill > 0) - .map(line => [ - line.id, - { - availableQuantity: line.quantity, - unitPrice: line.unitPrice.gross.amount, - selectedQuantity: 0, - }, - ]); + .map(toGrantRefundLine); const fulfilmentLines = order.fulfillments .flatMap(fulfilment => fulfilment.lines) - .map(line => [ - line.id, - { - availableQuantity: line.quantity, - unitPrice: line.orderLine.unitPrice.gross.amount, - selectedQuantity: 0, - }, - ]); + .map(toGrantRefundLine); return { lines: new Map([...unfulfilledLines, ...fulfilmentLines]), - refundShipping: false, + refundShipping: grantedRefund?.shippingCostsIncluded ?? false, }; }; @@ -75,15 +80,13 @@ export function grantRefundReducer( ): GrantRefundState { switch (action.type) { case "setQuantity": { - if (!state.lines.has(action.lineId)) { - return state; - } - const line = state.lines.get(action.lineId); const newLines = new Map(state.lines); newLines.set(action.lineId, { ...line, + isDirty: action.amount !== line.initialQuantity, + unitPrice: action.unitPrice, selectedQuantity: action.amount, }); @@ -92,16 +95,16 @@ export function grantRefundReducer( lines: newLines, }; } - case "setMaxQuantity": { const newLines = new Map(state.lines); - action.lineIds.forEach(lineId => { - const line = state.lines.get(lineId); - - newLines.set(lineId, { - ...line, - selectedQuantity: line.availableQuantity, + action.lines.forEach(line => { + const currentLine = state.lines.get(line.id); + newLines.set(line.id, { + ...currentLine, + isDirty: line.quantity !== currentLine.initialQuantity, + unitPrice: line.unitPrice, + selectedQuantity: line.quantity, }); }); @@ -110,19 +113,57 @@ export function grantRefundReducer( lines: newLines, }; } - case "initState": { return action.state; } - case "toggleRefundShipping": { return { ...state, refundShipping: !state.refundShipping, }; } + case "setRefundShipping": { + return { + ...state, + refundShipping: action.refundShipping, + }; + } default: exhaustiveCheck(action); } } + +function createToGrantRefundLineMap( + order: OrderDetailsGrantRefundFragment, + grantedRefund?: OrderDetailsGrantedRefundFragment, +) { + return ( + line: + | OrderDetailsGrantRefundFragment["lines"][0] + | OrderDetailsGrantRefundFragment["fulfillments"][0]["lines"][0], + ): GrantRefundLineKeyValue => { + const initialQuantity = + grantedRefund?.lines?.find(initLine => initLine.orderLine.id === line.id) + ?.quantity ?? 0; + + return [ + line.id, + { + isDirty: false, + availableQuantity: getLineAvailableQuantity({ + lineId: line.id, + lineQuntity: line.quantity, + grantRefunds: order?.grantedRefunds, + grantRefundId: grantedRefund?.id, + }), + unitPrice: + "orderLine" in line + ? line.orderLine.unitPrice.gross.amount + : line.unitPrice.gross.amount, + selectedQuantity: initialQuantity, + initialQuantity, + }, + ]; + }; +} diff --git a/src/orders/components/OrderGrantRefundPage/styles.ts b/src/orders/components/OrderGrantRefundPage/styles.ts index 0359a4c831d..36e8e38f6cb 100644 --- a/src/orders/components/OrderGrantRefundPage/styles.ts +++ b/src/orders/components/OrderGrantRefundPage/styles.ts @@ -1,81 +1,11 @@ import { makeStyles } from "@saleor/macaw-ui"; -export const useStyles = makeStyles( - theme => ({ - fulfilmentNumber: { - display: "inline", - marginLeft: theme.spacing(1), - }, - cardsContainer: { - display: "flex", - flexDirection: "column", - gap: theme.spacing(2), - }, - cardLoading: { - height: "20em", - }, - form: { - display: "contents", - }, - }), - { name: "OrderGrantRefund" }, -); - -export const useRefundCardStyles = makeStyles( - theme => ({ - cardContent: { - display: "flex", - flexDirection: "column", - gap: theme.spacing(1.5), - }, - refundCardHeader: { - paddingBottom: theme.spacing(1), - }, - suggestedValue: { - display: "flex", - alignItems: "baseline", - gap: theme.spacing(1), - flexWrap: "wrap", - }, - totalMoney: { - fontWeight: 600, - }, - applyButton: { - height: "auto", - padding: 0, - }, - shippingCostLine: { - display: "flex", - gap: theme.spacing(1), - "& .MuiCheckbox-root": { - padding: 0, - }, - }, - submitLine: { - display: "flex", - "& button": { - // when line overflows - marginLeft: "auto", - }, - }, - shippingCostLineLoading: { - height: "21px", - }, - }), - { name: "RefundCard" }, -); - export const useProductsCardStyles = makeStyles( theme => { - const inputPadding = { - paddingBottom: theme.spacing(2), - paddingTop: theme.spacing(2), - }; return { colProduct: { width: "auto", }, - productVariantName: {}, productName: { display: "flex", flexDirection: "column", @@ -92,14 +22,6 @@ export const useProductsCardStyles = makeStyles( textAlign: "right", width: `${75 + 32 + 32}px`, }, - quantityInnerInput: { - ...inputPadding, - }, - remainingQuantity: { - ...inputPadding, - color: theme.palette.text.secondary, - whiteSpace: "nowrap", - }, }; }, { name: "ProductsCard" }, diff --git a/src/orders/components/OrderGrantRefundPage/utils.test.ts b/src/orders/components/OrderGrantRefundPage/utils.test.ts new file mode 100644 index 00000000000..faa0d70c79f --- /dev/null +++ b/src/orders/components/OrderGrantRefundPage/utils.test.ts @@ -0,0 +1,161 @@ +import { OrderDetailsGrantedRefundFragment } from "@dashboard/graphql"; + +import { + calculateCanRefundShipping, + getRefundAmountValue, + OrderGrantRefundData, +} from "./utils"; + +describe("OrderGrantRefundPage utils", () => { + describe("calculateCanRefundShipping", () => { + it("should return true is current edited granted refund has granted refund for shipping", () => { + // Arrange + const editedGrantedRefund = { + grantRefundForShipping: true, + grantRefundId: "1", + } as OrderGrantRefundData; + + const grantedRefunds = [ + { + id: "1", + shippingCostsIncluded: true, + }, + { + id: "2", + shippingCostsIncluded: false, + }, + { + id: "3", + shippingCostsIncluded: false, + }, + ] as OrderDetailsGrantedRefundFragment[]; + + // Act + const canRefundShipping = calculateCanRefundShipping( + editedGrantedRefund, + grantedRefunds, + ); + + // Assert + expect(canRefundShipping).toBe(true); + }); + + it("should return true is current edited granted refund does not have grantend shipping refund but no other granted refund has greanted shipping", () => { + // Arrange + const editedGrantedRefund = { + grantRefundForShipping: false, + grantRefundId: "1", + } as OrderGrantRefundData; + + const grantedRefunds = [ + { + id: "1", + shippingCostsIncluded: false, + }, + { + id: "2", + shippingCostsIncluded: false, + }, + { + id: "3", + shippingCostsIncluded: false, + }, + ] as OrderDetailsGrantedRefundFragment[]; + + // Act + const canRefundShipping = calculateCanRefundShipping( + editedGrantedRefund, + grantedRefunds, + ); + + // Assert + expect(canRefundShipping).toBe(true); + }); + + it("should return true when there is no current edited granted refund and no other granted refund has greanted shipping", () => { + // Arrange + const grantedRefunds = [ + { + id: "1", + shippingCostsIncluded: false, + }, + { + id: "2", + shippingCostsIncluded: false, + }, + { + id: "3", + shippingCostsIncluded: false, + }, + ] as OrderDetailsGrantedRefundFragment[]; + + // Act + const canRefundShipping = calculateCanRefundShipping( + undefined, + grantedRefunds, + ); + + // Assert + expect(canRefundShipping).toBe(true); + }); + }); + + describe("getRefundAmountValue", () => { + it("should return refund amount when user provided value and input is dirty", () => { + // Arrange + const isAmountInputDirty = true; + const isEditedRefundAmount = false; + const totalCalulatedPrice = 15; + const refundAmount = 10; + + // Act + const refundAmountValue = getRefundAmountValue({ + isAmountInputDirty, + isEditedRefundAmount, + totalCalulatedPrice, + refundAmount, + }); + + // Assert + expect(refundAmountValue).toBe(refundAmount); + }); + + it("should return refund amount when user editing granted refund", () => { + // Arrange + const isAmountInputDirty = false; + const isEditedRefundAmount = true; + const totalCalulatedPrice = 15; + const refundAmount = 10; + + // Act + const refundAmountValue = getRefundAmountValue({ + isAmountInputDirty, + isEditedRefundAmount, + totalCalulatedPrice, + refundAmount, + }); + + // Assert + expect(refundAmountValue).toBe(refundAmount); + }); + + it("should return total calculated when user create granted refund and change quantity or shipping ", () => { + // Arrange + const isAmountInputDirty = false; + const isEditedRefundAmount = false; + const totalCalulatedPrice = 25; + const refundAmount = 0; + + // Act + const refundAmountValue = getRefundAmountValue({ + isAmountInputDirty, + isEditedRefundAmount, + totalCalulatedPrice, + refundAmount, + }); + + // Assert + expect(refundAmountValue).toBe(totalCalulatedPrice); + }); + }); +}); diff --git a/src/orders/components/OrderGrantRefundPage/utils.ts b/src/orders/components/OrderGrantRefundPage/utils.ts index 4602f3615e1..760c4e94992 100644 --- a/src/orders/components/OrderGrantRefundPage/utils.ts +++ b/src/orders/components/OrderGrantRefundPage/utils.ts @@ -1,7 +1,12 @@ -import { OrderDetailsGrantRefundFragment } from "@dashboard/graphql"; +import { + OrderDetailsGrantedRefundFragment, + OrderDetailsGrantRefundFragment, + OrderGrantRefundCreateLineInput, +} from "@dashboard/graphql"; import currency from "currency.js"; -import { GrantRefundState } from "./reducer"; +import { Line } from "./form"; +import { GrantRefundState, ReducerOrderLine } from "./reducer"; export const calculateTotalPrice = ( state: GrantRefundState, @@ -26,3 +31,99 @@ export const getFulfilmentSubtitle = ( order: OrderDetailsGrantRefundFragment, fulfillment: OrderDetailsGrantRefundFragment["fulfillments"][0], ) => `#${order.number}-${fulfillment.fulfillmentOrder}`; + +export const prepareLineData = (lines: Map): Line[] => + Array.from(lines.entries()) + .filter(([_, line]) => line.isDirty) + .map(([id, line]) => ({ + id, + quantity: line.selectedQuantity, + })); + +export const getLineAvailableQuantity = ({ + lineId, + lineQuntity, + grantRefunds, + grantRefundId, +}: { + lineId: string; + lineQuntity: number; + grantRefunds: OrderDetailsGrantRefundFragment["grantedRefunds"]; + grantRefundId?: string; +}) => { + let refundedQuantity = 0; + + grantRefunds.forEach(refund => { + if (grantRefundId && refund.id === grantRefundId) { + return; + } + + refund?.lines?.forEach(line => { + if (line.orderLine.id === lineId) { + refundedQuantity += line.quantity; + } + }); + }); + + return lineQuntity - refundedQuantity; +}; + +export interface OrderGrantRefundData { + amount: number; + reason: string; + lines: OrderGrantRefundCreateLineInput[]; + grantRefundForShipping: boolean; + grantRefundId: string; +} + +export const getGrantedRefundData = ( + grantedRefund?: OrderDetailsGrantedRefundFragment, +): OrderGrantRefundData | undefined => { + if (!grantedRefund) { + return undefined; + } + + return { + grantRefundId: grantedRefund.id, + reason: grantedRefund?.reason ?? "", + amount: grantedRefund.amount.amount, + grantRefundForShipping: grantedRefund.shippingCostsIncluded, + lines: grantedRefund?.lines ?? [], + }; +}; + +export const calculateCanRefundShipping = ( + editedGrantedRefund?: OrderGrantRefundData, + grantedRefunds?: OrderDetailsGrantedRefundFragment[], +) => { + if (editedGrantedRefund) { + if (editedGrantedRefund.grantRefundForShipping) { + return true; + } + return !grantedRefunds?.some( + refund => + refund.shippingCostsIncluded && + refund.id !== editedGrantedRefund.grantRefundId, + ); + } + return !grantedRefunds?.some(refund => refund.shippingCostsIncluded); +}; + +export const getRefundAmountValue = ({ + isAmountInputDirty, + isEditedRefundAmount, + totalCalulatedPrice, + refundAmount, +}: { + isAmountInputDirty: boolean; + totalCalulatedPrice: number; + refundAmount: number; + isEditedRefundAmount: boolean; +}) => { + // User provided value into input or we are editing refund amount + if (isAmountInputDirty || isEditedRefundAmount) { + return refundAmount; + } + + return totalCalulatedPrice ?? 0; +}; diff --git a/src/orders/mutations.ts b/src/orders/mutations.ts index 4b682f312ae..81ab9dbdf50 100644 --- a/src/orders/mutations.ts +++ b/src/orders/mutations.ts @@ -510,12 +510,19 @@ export const orderTransactionRequestActionMutation = gql` export const orderGrantRefundAddMutation = gql` mutation OrderGrantRefundAdd( $orderId: ID! - $amount: Decimal! + $amount: Decimal $reason: String + $lines: [OrderGrantRefundCreateLineInput!] + $grantRefundForShipping: Boolean ) { orderGrantRefundCreate( id: $orderId - input: { amount: $amount, reason: $reason } + input: { + amount: $amount + reason: $reason + lines: $lines + grantRefundForShipping: $grantRefundForShipping + } ) { errors { ...OrderGrantRefundCreateError @@ -527,12 +534,21 @@ export const orderGrantRefundAddMutation = gql` export const orderGrantRefundEditMutation = gql` mutation OrderGrantRefundEdit( $refundId: ID! - $amount: Decimal! + $amount: Decimal $reason: String + $addLines: [OrderGrantRefundUpdateLineAddInput!] + $removeLines: [ID!] + $grantRefundForShipping: Boolean ) { orderGrantRefundUpdate( id: $refundId - input: { amount: $amount, reason: $reason } + input: { + amount: $amount + reason: $reason + addLines: $addLines + removeLines: $removeLines + grantRefundForShipping: $grantRefundForShipping + } ) { errors { ...OrderGrantRefundUpdateError diff --git a/src/orders/queries.ts b/src/orders/queries.ts index 4e975c10a31..4ec36cb6c0f 100644 --- a/src/orders/queries.ts +++ b/src/orders/queries.ts @@ -149,13 +149,6 @@ export const orderDetailsGrantedRefundEdit = gql` query OrderDetailsGrantRefundEdit($id: ID!) { order(id: $id) { ...OrderDetailsGrantRefund - grantedRefunds { - id - reason - amount { - ...Money - } - } } } `; diff --git a/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx b/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx index 305557056ff..e78a6b2af49 100644 --- a/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx +++ b/src/orders/views/OrderEditGrantRefund/OrderEditGrantRefund.tsx @@ -50,13 +50,45 @@ const OrderEditGrantRefund: React.FC = ({ }, }); - const handleSubmit = async ({ amount, reason }: OrderGrantRefundFormData) => { - extractMutationErrors( + const handleSubmit = async ({ + amount, + reason, + lines, + grantRefundForShipping, + }: OrderGrantRefundFormData) => { + const grantedRefundLinesToDelete = lines + .map(line => + grantedRefund.lines.find( + grandLine => grandLine.orderLine.id === line.id, + ), + ) + .filter(Boolean) + .map(line => line.id); + + if (grantedRefundLinesToDelete.length > 0) { + await extractMutationErrors( + grantRefund({ + variables: { + refundId: grantRefundId, + removeLines: grantedRefundLinesToDelete, + }, + }), + ); + } + + await extractMutationErrors( grantRefund({ variables: { refundId: grantRefundId, amount, reason, + grantRefundForShipping, + addLines: lines.map(line => ({ + id: line.id, + quantity: line.quantity, + reason: line.reason ?? "", + })), + removeLines: [], }, }), ); @@ -67,7 +99,7 @@ const OrderEditGrantRefund: React.FC = ({ undefined} isEdit /> @@ -93,10 +125,7 @@ const OrderEditGrantRefund: React.FC = ({ submitState={grantRefundOptions.status} onSubmit={handleSubmit} isEdit - initialData={{ - reason: grantedRefund.reason, - amount: grantedRefund.amount.amount.toString(), - }} + initialData={grantedRefund} /> ); diff --git a/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx b/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx index 3503af2751c..ac03c89e5d1 100644 --- a/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx +++ b/src/orders/views/OrderGrantRefund/OrderGrantRefund.tsx @@ -45,13 +45,26 @@ const OrderGrantRefund: React.FC = ({ orderId }) => { }, }); - const handleSubmit = async ({ amount, reason }: OrderGrantRefundFormData) => { + const handleSubmit = async ({ + amount, + reason, + lines, + grantRefundForShipping, + }: OrderGrantRefundFormData) => { + // API call should be stoped when use doesn't select any line, + // doesn't provide own amount and doesn't want to refund shipping + if (lines.length === 0 && amount === undefined && !grantRefundForShipping) { + return; + } + extractMutationErrors( grantRefund({ variables: { orderId, amount, reason, + lines, + grantRefundForShipping, }, }), ); diff --git a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx index e985f5f0988..5bd70445309 100644 --- a/src/pages/components/PageDetailsPage/PageDetailsPage.tsx +++ b/src/pages/components/PageDetailsPage/PageDetailsPage.tsx @@ -249,6 +249,9 @@ const PageDetailsPage: React.FC = ({ assignReferencesAttributeId, data.attributes, )} + attribute={data.attributes.find( + ({ id }) => id === assignReferencesAttributeId, + )} confirmButtonState={"default"} products={referenceProducts} pages={referencePages} diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index c2fa3878bd0..86c67fb362e 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -385,6 +385,9 @@ export const ProductCreatePage: React.FC = ({ confirmButtonState={"default"} products={referenceProducts} pages={referencePages} + attribute={data.attributes.find( + ({ id }) => id === assignReferencesAttributeId, + )} hasMore={handlers.fetchMoreReferences?.hasMore} open={canOpenAssignReferencesAttributeDialog} onFetch={handlers.fetchReferences} diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index 9b358fa1cda..b5d1af22e86 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -449,6 +449,9 @@ export const ProductUpdatePage: React.FC = ({ confirmButtonState={"default"} products={referenceProducts} pages={referencePages} + attribute={data.attributes.find( + ({ id }) => id === assignReferencesAttributeId, + )} hasMore={handlers.fetchMoreReferences?.hasMore} open={canOpenAssignReferencesAttributeDialog} onFetch={handlers.fetchReferences} diff --git a/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx b/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx index 6b4b09e0206..ac2bec1e1f8 100644 --- a/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx +++ b/src/products/components/ProductVariantCreatePage/ProductVariantCreatePage.tsx @@ -323,6 +323,9 @@ const ProductVariantCreatePage: React.FC = ({ confirmButtonState={"default"} products={referenceProducts} pages={referencePages} + attribute={data.attributes.find( + ({ id }) => id === assignReferencesAttributeId, + )} hasMore={handlers.fetchMoreReferences?.hasMore} open={canOpenAssignReferencesAttributeDialog} onFetch={handlers.fetchReferences} diff --git a/src/products/components/ProductVariantPage/ProductVariantPage.tsx b/src/products/components/ProductVariantPage/ProductVariantPage.tsx index 28f382e6776..6c46a569fd3 100644 --- a/src/products/components/ProductVariantPage/ProductVariantPage.tsx +++ b/src/products/components/ProductVariantPage/ProductVariantPage.tsx @@ -392,6 +392,9 @@ const ProductVariantPage: React.FC = ({ confirmButtonState={"default"} products={referenceProducts} pages={referencePages} + attribute={data.attributes.find( + ({ id }) => id === assignReferencesAttributeId, + )} hasMore={handlers.fetchMoreReferences?.hasMore} open={canOpenAssignReferencesAttributeDialog} onFetch={handlers.fetchReferences} diff --git a/src/products/components/ProductVariants/utils.tsx b/src/products/components/ProductVariants/utils.tsx index 171c8070f91..8b43b22bb32 100644 --- a/src/products/components/ProductVariants/utils.tsx +++ b/src/products/components/ProductVariants/utils.tsx @@ -117,7 +117,7 @@ export function getData({ const change = changes.current[getChangeIndex(columnId, row)]?.data; const dataRow = added.includes(row) ? undefined - : variants[row + removed.filter(r => r <= row).length]; + : variants.filter((_, index) => !removed.includes(index))[row]; switch (columnId) { case "name": diff --git a/src/products/fixtures.ts b/src/products/fixtures.ts index fcd15c92926..9e30dfab3b8 100644 --- a/src/products/fixtures.ts +++ b/src/products/fixtures.ts @@ -1044,6 +1044,182 @@ export const product: ( quantityLimitPerCustomer: null, __typename: "ProductVariant", }, + { + id: "UHJvZHVjdFZhcmlhbnQ6MjA0", + sku: "76432981", + name: "750ml", + margin: 0.25, + attributes: [ + { + attribute: { + id: "QXR0cmlidXRlOjE2", + name: "Bottle Size", + __typename: "Attribute", + }, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6NTU=", + name: "750ml", + plainText: "", + richText: "", + slug: "", + reference: "", + boolean: false, + date: "", + dateTime: "", + value: "", + file: { + __typename: "File", + url: "", + contentType: "", + }, + __typename: "AttributeValue", + }, + ], + __typename: "SelectedAttribute", + }, + ], + media: [ + { + id: "2", + type: ProductMediaType.VIDEO, + url: "randomVideoUrl", + __typename: "ProductMedia", + }, + ], + stocks: [ + { + id: "U3RvY2s6MTYz", + quantity: 600, + quantityAllocated: 50, + warehouse: { + id: "V2FyZWhvdXNlOjEwM2VjNzY2LTA1NmItNDU2My05YjQzLTUxYmU5ZGJmNGEzYQ==", + name: "Warehouse-123", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + ], + trackInventory: true, + preorder: null, + channelListings: [ + { + id: "UHJvZHVjdFZhcmlhbnRDaSAD3w2FubmVsTGlzdGluZzoyNzU=", + channel: { + id: "Q2hhbm5lbDox", + name: "Channel-EUR", + currencyCode: "EUR", + __typename: "Channel", + }, + price: { + amount: 7.5, + currency: "EUR", + __typename: "Money", + }, + costPrice: { + amount: 2.5, + currency: "EUR", + __typename: "Money", + }, + preorderThreshold: { + quantity: null, + soldUnits: 0, + __typename: "PreorderThreshold", + }, + __typename: "ProductVariantChannelListing", + }, + ], + quantityLimitPerCustomer: 5, + __typename: "ProductVariant", + }, + { + id: "UHJvZHVjdFZhcmlhbnQ6MjA1", + sku: "12345678", + name: "1 Liter", + margin: 0.15, + attributes: [ + { + attribute: { + id: "QXR0cmlidXRlOjE3", + name: "Bottle Size", + __typename: "Attribute", + }, + values: [ + { + id: "QXR0cmlidXRlVmFsdWU6NjU=", + name: "1 Liter", + plainText: "", + richText: "", + slug: "", + reference: "", + boolean: false, + date: "", + dateTime: "", + value: "", + file: { + __typename: "File", + url: "", + contentType: "", + }, + __typename: "AttributeValue", + }, + ], + __typename: "SelectedAttribute", + }, + ], + media: [ + { + id: "3", + type: ProductMediaType.IMAGE, + url: "randomImageUrl", + __typename: "ProductMedia", + }, + ], + stocks: [ + { + id: "U3RvY2s6MTY0", + quantity: 800, + quantityAllocated: 100, + warehouse: { + id: "V2FyZWhvdXNlOjExNmQ2NGYyLTZhOGYtNGE4MC1iNmJkLTk1MDg4YTliZDEwYQ==", + name: "Warehouse-456", + __typename: "Warehouse", + }, + __typename: "Stock", + }, + ], + trackInventory: true, + preorder: null, + channelListings: [ + { + id: "UHJvZHVjdFZhcmlhbnRDaSAD3w2FubmVsTGlzdGluZzoyNzY=", + channel: { + id: "Q2hhbm5lbDoy", + name: "Channel-GBP", + currencyCode: "GBP", + __typename: "Channel", + }, + price: { + amount: 10.0, + currency: "GBP", + __typename: "Money", + }, + costPrice: { + amount: 2.0, + currency: "GBP", + __typename: "Money", + }, + preorderThreshold: { + quantity: null, + soldUnits: 0, + __typename: "PreorderThreshold", + }, + __typename: "ProductVariantChannelListing", + }, + ], + quantityLimitPerCustomer: null, + __typename: "ProductVariant", + }, ], visibleInListings: true, weight: { diff --git a/src/products/utils/datagrid.test.ts b/src/products/utils/datagrid.test.ts deleted file mode 100644 index 34765b9a517..00000000000 --- a/src/products/utils/datagrid.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -// @ts-strict-ignore -import { isCurrentRow } from "./datagrid"; - -describe("isCurrentRow", () => { - test("should return true when variant index is equal to datagrid row index and no removed rows", () => { - // Arrange & Act - const datagridChangeRowIndex = 1; - const variantIndex = 1; - const datagridRemoveRowsIds = []; - - // Assert - expect( - isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds), - ).toEqual(true); - }); - - test("should return true when variant index is equal to datagrid row index and removed rows contain higher rows ids", () => { - // Arrange & Act - const datagridChangeRowIndex = 1; - const variantIndex = 1; - const datagridRemoveRowsIds = [4, 5, 6]; - - // Assert - expect( - isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds), - ).toEqual(true); - }); - - test("should return false when variant index is not equal to datagrid row index and removed rows contains prev row id", () => { - // Arrange & Act - const datagridChangeRowIndex = 2; - const variantIndex = 1; - const datagridRemoveRowsIds = [1]; - - // Assert - expect( - isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds), - ).toEqual(true); - }); - - test("should return false when variant index is not equal to datagrid row index ", () => { - // Arrange & Act - const datagridChangeRowIndex = 1; - const variantIndex = 2; - const datagridRemoveRowsIds = []; - - // Assert - expect( - isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds), - ).toEqual(false); - }); - - test("should return false when variant index is equal to datagrid row index and removed rows contains prev row id", () => { - // Arrange & Act - const datagridChangeRowIndex = 2; - const variantIndex = 2; - const datagridRemoveRowsIds = [1]; - - // Assert - expect( - isCurrentRow(datagridChangeRowIndex, variantIndex, datagridRemoveRowsIds), - ).toEqual(false); - }); -}); diff --git a/src/products/utils/datagrid.ts b/src/products/utils/datagrid.ts index c414f2f235a..fc14ef5d007 100644 --- a/src/products/utils/datagrid.ts +++ b/src/products/utils/datagrid.ts @@ -26,8 +26,4 @@ export const getColumnName = (column: string) => { export const isCurrentRow = ( datagridChangeIndex: number, variantIndex: number, - datagridRemovedRowsIds: number[], -) => - datagridChangeIndex === - variantIndex + - datagridRemovedRowsIds.filter(index => index <= variantIndex).length; +) => datagridChangeIndex === variantIndex; diff --git a/src/products/views/ProductUpdate/handlers/data/attributes.test.ts b/src/products/views/ProductUpdate/handlers/data/attributes.test.ts index d999f9a3b12..fe623d0684d 100644 --- a/src/products/views/ProductUpdate/handlers/data/attributes.test.ts +++ b/src/products/views/ProductUpdate/handlers/data/attributes.test.ts @@ -20,7 +20,7 @@ describe("getAttributeData", () => { ]; // Act - const attributes = getAttributeData(changeData, 1, [], variantAttributes); + const attributes = getAttributeData(changeData, 1, variantAttributes); // Assert expect(attributes).toEqual([ @@ -42,7 +42,7 @@ describe("getAttributeData", () => { ]; // Act - const attributes = getAttributeData(changeData, 2, [], variantAttributes); + const attributes = getAttributeData(changeData, 2, variantAttributes); // Assert expect(attributes).toEqual([]); @@ -56,7 +56,7 @@ describe("getAttributeData", () => { ]; // Act - const attributes = getAttributeData(changeData, 1, [], variantAttributes); + const attributes = getAttributeData(changeData, 1, variantAttributes); // Assert expect(attributes).toEqual([]); diff --git a/src/products/views/ProductUpdate/handlers/data/attributes.ts b/src/products/views/ProductUpdate/handlers/data/attributes.ts index 4a2169803b5..68a1517d877 100644 --- a/src/products/views/ProductUpdate/handlers/data/attributes.ts +++ b/src/products/views/ProductUpdate/handlers/data/attributes.ts @@ -16,11 +16,10 @@ import { byAttributeName } from "../utils"; export function getAttributeData( data: DatagridChange[], currentIndex: number, - removedIds: number[], variantAttributes: VariantAttributeFragment[], ) { return data - .filter(change => isCurrentRow(change.row, currentIndex, removedIds)) + .filter(change => isCurrentRow(change.row, currentIndex)) .filter(byHavingAnyAttribute) .map(toAttributeData(variantAttributes)); } diff --git a/src/products/views/ProductUpdate/handlers/data/channel.ts b/src/products/views/ProductUpdate/handlers/data/channel.ts index 808ea662573..d01adb43214 100644 --- a/src/products/views/ProductUpdate/handlers/data/channel.ts +++ b/src/products/views/ProductUpdate/handlers/data/channel.ts @@ -19,7 +19,7 @@ export function getUpdateVariantChannelInputs( variant: ProductFragment["variants"][number], ): ProductVariantChannelListingUpdateInput { return data.updates - .filter(byCurrentRowByIndex(index, data)) + .filter(byCurrentRowByIndex(index)) .map(availabilityToChannelColumn) .filter(byChannelColumn) .reduce(byColumn, []) @@ -39,7 +39,7 @@ export function getVariantChannelsInputs( index: number, ): ProductVariantChannelListingAddInput[] { return data.updates - .filter(byCurrentRowByIndex(index, data)) + .filter(byCurrentRowByIndex(index)) .map(availabilityToChannelColumn) .filter(byChannelColumn) .reduce(byColumn, []) @@ -47,10 +47,9 @@ export function getVariantChannelsInputs( .filter(byNotNullPrice); } -function byCurrentRowByIndex(index: number, data: DatagridChangeOpts) { +function byCurrentRowByIndex(index: number) { return (change: DatagridChange) => { - const totalRemoved = data.removed.filter(r => r <= index).length; - return change.row === index + totalRemoved; + return change.row === index; }; } diff --git a/src/products/views/ProductUpdate/handlers/data/name.test.ts b/src/products/views/ProductUpdate/handlers/data/name.test.ts index 177c26387e2..bdec06b9f48 100644 --- a/src/products/views/ProductUpdate/handlers/data/name.test.ts +++ b/src/products/views/ProductUpdate/handlers/data/name.test.ts @@ -11,7 +11,7 @@ describe("getNameData", () => { ]; // Act - const name = getNameData(changeData, 1, []); + const name = getNameData(changeData, 1); // Assert expect(name).toEqual("Joe"); @@ -24,7 +24,7 @@ describe("getNameData", () => { ]; // Act - const name = getNameData(changeData, 1, []); + const name = getNameData(changeData, 1); // Assert expect(name).toEqual(undefined); @@ -37,7 +37,7 @@ describe("getNameData", () => { ]; // Act - const name = getNameData(changeData, 1, []); + const name = getNameData(changeData, 1); // Assert expect(name).toEqual(undefined); diff --git a/src/products/views/ProductUpdate/handlers/data/name.ts b/src/products/views/ProductUpdate/handlers/data/name.ts index bcf2895f8ec..91dba91d2ed 100644 --- a/src/products/views/ProductUpdate/handlers/data/name.ts +++ b/src/products/views/ProductUpdate/handlers/data/name.ts @@ -4,11 +4,9 @@ import { isCurrentRow } from "@dashboard/products/utils/datagrid"; export function getNameData( data: DatagridChange[], currentIndex: number, - removedIds: number[], ): string | undefined { return data.find( change => - change.column === "name" && - isCurrentRow(change.row, currentIndex, removedIds), + change.column === "name" && isCurrentRow(change.row, currentIndex), )?.data; } diff --git a/src/products/views/ProductUpdate/handlers/data/sku.test.ts b/src/products/views/ProductUpdate/handlers/data/sku.test.ts index b8d45b00038..edb997417c1 100644 --- a/src/products/views/ProductUpdate/handlers/data/sku.test.ts +++ b/src/products/views/ProductUpdate/handlers/data/sku.test.ts @@ -11,7 +11,7 @@ describe("getSkuData", () => { ]; // Act - const name = getSkuData(changeData, 1, []); + const name = getSkuData(changeData, 1); // Assert expect(name).toEqual("123"); @@ -24,7 +24,7 @@ describe("getSkuData", () => { ]; // Act - const name = getSkuData(changeData, 1, []); + const name = getSkuData(changeData, 1); // Assert expect(name).toEqual(undefined); @@ -37,7 +37,7 @@ describe("getSkuData", () => { ]; // Act - const name = getSkuData(changeData, 1, []); + const name = getSkuData(changeData, 1); // Assert expect(name).toEqual(undefined); diff --git a/src/products/views/ProductUpdate/handlers/data/sku.ts b/src/products/views/ProductUpdate/handlers/data/sku.ts index 969d5796c37..44aaa7fc52d 100644 --- a/src/products/views/ProductUpdate/handlers/data/sku.ts +++ b/src/products/views/ProductUpdate/handlers/data/sku.ts @@ -4,11 +4,8 @@ import { isCurrentRow } from "@dashboard/products/utils/datagrid"; export function getSkuData( data: DatagridChange[], currentIndex: number, - removedIds: number[], ): string | undefined { return data.find( - change => - change.column === "sku" && - isCurrentRow(change.row, currentIndex, removedIds), + change => change.column === "sku" && isCurrentRow(change.row, currentIndex), )?.data; } diff --git a/src/products/views/ProductUpdate/handlers/data/stock.ts b/src/products/views/ProductUpdate/handlers/data/stock.ts index bf5f56f443b..0c491160ce1 100644 --- a/src/products/views/ProductUpdate/handlers/data/stock.ts +++ b/src/products/views/ProductUpdate/handlers/data/stock.ts @@ -10,13 +10,9 @@ import { isCurrentRow, } from "@dashboard/products/utils/datagrid"; -export function getStockData( - data: DatagridChange[], - currentIndex: number, - removedIds: number[], -) { +export function getStockData(data: DatagridChange[], currentIndex: number) { return data - .filter(change => byHavingStockColumn(change, currentIndex, removedIds)) + .filter(change => byHavingStockColumn(change, currentIndex)) .map(toStockData) .filter(byStockWithQuantity); } @@ -24,11 +20,10 @@ export function getStockData( export function getVaraintUpdateStockData( data: DatagridChange[], currentIndex: number, - removedIds: number[], variant: ProductFragment["variants"][number], ) { return data - .filter(change => byHavingStockColumn(change, currentIndex, removedIds)) + .filter(change => byHavingStockColumn(change, currentIndex)) .map(toStockData) .reduce(toUpdateStockData(variant), { create: [], @@ -77,13 +72,8 @@ function byStockWithQuantity(stock: { quantity: unknown }) { return stock.quantity !== numberCellEmptyValue; } -function byHavingStockColumn( - change: DatagridChange, - currentIndex: number, - removedIds: number[], -) { +function byHavingStockColumn(change: DatagridChange, currentIndex: number) { return ( - getColumnStock(change.column) && - isCurrentRow(change.row, currentIndex, removedIds) + getColumnStock(change.column) && isCurrentRow(change.row, currentIndex) ); } diff --git a/src/products/views/ProductUpdate/handlers/data/stocks.test.ts b/src/products/views/ProductUpdate/handlers/data/stocks.test.ts index 15d12b46094..547e7a98e36 100644 --- a/src/products/views/ProductUpdate/handlers/data/stocks.test.ts +++ b/src/products/views/ProductUpdate/handlers/data/stocks.test.ts @@ -20,7 +20,7 @@ describe("getStockData", () => { ]; // Act - const stocks = getStockData(changeData, 1, []); + const stocks = getStockData(changeData, 1); // Assert expect(stocks).toEqual([ @@ -42,7 +42,7 @@ describe("getStockData", () => { ]; // Act - const stocks = getStockData(changeData, 1, []); + const stocks = getStockData(changeData, 1); // Assert expect(stocks).toEqual([]); @@ -55,7 +55,7 @@ describe("getStockData", () => { ]; // Act - const stocks = getStockData(changeData, 1, []); + const stocks = getStockData(changeData, 1); // Assert expect(stocks).toEqual([]); @@ -86,7 +86,7 @@ describe("getVaraintUpdateStockData", () => { ]; // Act - const variantStocks = getVaraintUpdateStockData(changeData, 1, [], { + const variantStocks = getVaraintUpdateStockData(changeData, 1, { stocks, } as ProductFragment["variants"][number]); @@ -123,7 +123,7 @@ describe("getVaraintUpdateStockData", () => { ]; // Act - const variantStocks = getVaraintUpdateStockData(changeData, 1, [], { + const variantStocks = getVaraintUpdateStockData(changeData, 1, { stocks, } as ProductFragment["variants"][number]); @@ -146,7 +146,7 @@ describe("getVaraintUpdateStockData", () => { ]; // Act - const variantStocks = getVaraintUpdateStockData(changeData, 1, [], { + const variantStocks = getVaraintUpdateStockData(changeData, 1, { stocks, } as ProductFragment["variants"][number]); @@ -174,7 +174,7 @@ describe("getVaraintUpdateStockData", () => { ]; // Act - const variantStocks = getVaraintUpdateStockData(changeData, 1, [], { + const variantStocks = getVaraintUpdateStockData(changeData, 1, { stocks, } as ProductFragment["variants"][number]); @@ -189,7 +189,7 @@ describe("getVaraintUpdateStockData", () => { ]; // Act - const variantStocks = getVaraintUpdateStockData(changeData, 1, [], { + const variantStocks = getVaraintUpdateStockData(changeData, 1, { stocks, } as ProductFragment["variants"][number]); diff --git a/src/products/views/ProductUpdate/handlers/utils.test.ts b/src/products/views/ProductUpdate/handlers/utils.test.ts index 1e3e309f0df..05f74eb6a95 100644 --- a/src/products/views/ProductUpdate/handlers/utils.test.ts +++ b/src/products/views/ProductUpdate/handlers/utils.test.ts @@ -358,4 +358,96 @@ describe("getBulkVariantUpdateInputs", () => { }, ]); }); + test("should return input data base on datagrid change data for simultaneous bulk operations", () => { + // Arrange + const variants: ProductFragment["variants"] = + product("http://google.com").variants; + const inputData: DatagridChangeOpts = { + updates: [ + { + data: "2345555", + column: "sku", + row: 0, // initially 0 + }, + { + data: { + kind: "money-cell", + value: 234, + currency: "USD", + }, + column: `channel:${variants[0].channelListings[0].channel.id}`, + row: 0, // initially 0 + }, + { + data: "edited variant", + column: "name", + row: 1, // initially 2 + }, + { + data: { + kind: "number-cell", + value: 2344, + }, + column: `warehouse:${variants[2].stocks[0].warehouse.id}`, + row: 1, // initially 2 + }, + // row 2 (initially 4) is unchanged + { + data: "completely new variant", + column: "name", + row: 3, // initially 5 + }, + ], + // DatagridChangeOpts generates removed indices based on initial grid, + // meanwhile added and updates indices are calculated on the grid after removal + // of rows. This is why we have 3 as an index both in removed and added. + removed: [1, 3], + added: [3], + }; + + // Act + const bulkVariantUpdateInput = getBulkVariantUpdateInputs( + variants, + inputData, + variantAttributes, + ); + + // Assert + expect(bulkVariantUpdateInput).toEqual([ + { + id: variants[0].id, + attributes: [], + sku: "2345555", + name: undefined, + stocks: { create: [], update: [], remove: [] }, + channelListings: { + create: [], + remove: [], + update: [ + { + channelListing: variants[0].channelListings[0].id, + price: 234, + }, + ], + }, + }, + { + id: variants[2].id, + attributes: [], + sku: undefined, + name: "edited variant", + stocks: { + create: [], + update: [ + { + stock: variants[2].stocks[0].id, + quantity: 2344, + }, + ], + remove: [], + }, + channelListings: { create: [], remove: [], update: [] }, + }, + ]); + }); }); diff --git a/src/products/views/ProductUpdate/handlers/utils.ts b/src/products/views/ProductUpdate/handlers/utils.ts index de3e5a802f6..3678d1cdf30 100644 --- a/src/products/views/ProductUpdate/handlers/utils.ts +++ b/src/products/views/ProductUpdate/handlers/utils.ts @@ -72,16 +72,11 @@ export function getCreateVariantInput( variantAttributes: VariantAttributeFragment[], ) { return { - attributes: getAttributeData( - data.updates, - index, - data.removed, - variantAttributes, - ), - sku: getSkuData(data.updates, index, data.removed), - name: getNameData(data.updates, index, data.removed), + attributes: getAttributeData(data.updates, index, variantAttributes), + sku: getSkuData(data.updates, index), + name: getNameData(data.updates, index), channelListings: getVariantChannelsInputs(data, index), - stocks: getStockData(data.updates, index, data.removed), + stocks: getStockData(data.updates, index), }; } @@ -151,7 +146,11 @@ export function getBulkVariantUpdateInputs( variantsAttributes: VariantAttributeFragment[], ): ProductVariantBulkUpdateInput[] { const toUpdateInput = createToUpdateInput(data, variantsAttributes); - return variants.map(toUpdateInput).filter(byAvailability); + return variants + .filter((_, index) => !data.removed.includes(index)) + .map(toUpdateInput) + .filter(byAvailability) + .filter((_, index) => !data.added.includes(index)); } const createToUpdateInput = @@ -167,14 +166,9 @@ const createToUpdateInput = variant, variantsAttributes, ), - sku: getSkuData(data.updates, variantIndex, data.removed), - name: getNameData(data.updates, variantIndex, data.removed), - stocks: getVaraintUpdateStockData( - data.updates, - variantIndex, - data.removed, - variant, - ), + sku: getSkuData(data.updates, variantIndex), + name: getNameData(data.updates, variantIndex), + stocks: getVaraintUpdateStockData(data.updates, variantIndex, variant), channelListings: getUpdateVariantChannelInputs(data, variantIndex, variant), }); @@ -187,7 +181,6 @@ const getVariantAttributesForUpdate = ( const updatedAttributes = getAttributeData( data.updates, variantIndex, - data.removed, variantsAttributes, ); diff --git a/src/types.ts b/src/types.ts index 8b7371e4c9f..e8d2329c892 100644 --- a/src/types.ts +++ b/src/types.ts @@ -258,6 +258,5 @@ export enum StatusType { SUCCESS = "success", } -export type RelayToFlat }> = Array< - T["edges"][0]["node"] ->; +export type RelayToFlat } | null> = + T extends { edges: Array<{ node: infer U }> } ? U[] : null; diff --git a/testUtils/setup.ts b/testUtils/setup.ts index 7b59426471e..ab7d5d975b9 100644 --- a/testUtils/setup.ts +++ b/testUtils/setup.ts @@ -27,6 +27,7 @@ window.__SALEOR_CONFIG__ = { APPS_MARKETPLACE_API_URI: "http://localhost:3000", APPS_TUNNEL_URL_KEYWORDS: ".ngrok.io;.saleor.live", IS_CLOUD_INSTANCE: "true", + LOCALE_CODE: "EN", }; process.env.TZ = "UTC"; diff --git a/types.d.ts b/types.d.ts index 42d5ab3618e..1780c197f21 100644 --- a/types.d.ts +++ b/types.d.ts @@ -13,6 +13,7 @@ declare interface Window { __SALEOR_CONFIG__: { API_URL: string; APP_MOUNT_URI: string; + LOCALE_CODE?: string; APPS_MARKETPLACE_API_URI?: string; APPS_TUNNEL_URL_KEYWORDS?: string; IS_CLOUD_INSTANCE?: string; diff --git a/vite.config.js b/vite.config.js index e178e008aea..b41a06b4405 100644 --- a/vite.config.js +++ b/vite.config.js @@ -41,6 +41,7 @@ export default defineConfig(({ command, mode }) => { DEMO_MODE, CUSTOM_VERSION, FLAGS_SERVICE_ENABLED, + LOCALE_CODE, } = env; const base = STATIC_URL ?? "/"; @@ -62,6 +63,7 @@ export default defineConfig(({ command, mode }) => { APPS_MARKETPLACE_API_URI, APPS_TUNNEL_URL_KEYWORDS, IS_CLOUD_INSTANCE, + LOCALE_CODE, injectOgTags: DEMO_MODE && ` @@ -142,6 +144,7 @@ export default defineConfig(({ command, mode }) => { ENVIRONMENT, DEMO_MODE, CUSTOM_VERSION, + LOCALE_CODE, }, }, build: {