Merge branch 'feature/35956-mcm-library-changes' into feature/34556-marketing-api
This commit is contained in:
commit
4ef2d5781f
|
@ -1,28 +1,28 @@
|
|||
name: Cherry Pick Tool
|
||||
on:
|
||||
issues:
|
||||
types: [milestoned, labeled]
|
||||
pull_request:
|
||||
types: [closed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_branch:
|
||||
description: Provide the release branch you want to cherry pick into. Example release/6.9
|
||||
default: ''
|
||||
required: true
|
||||
pull_requests:
|
||||
description: The pull request number.
|
||||
default: ''
|
||||
required: true
|
||||
skipSlackPing:
|
||||
description: "Skip Slack Ping: If true, the Slack ping will be skipped (useful for testing)"
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
slackChannelOverride:
|
||||
description: "Slack Channel Override: The channel ID to send the Slack ping about the code freeze violation"
|
||||
required: false
|
||||
default: ''
|
||||
issues:
|
||||
types: [milestoned, labeled]
|
||||
pull_request:
|
||||
types: [closed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_branch:
|
||||
description: Provide the release branch you want to cherry pick into. Example release/6.9
|
||||
default: ''
|
||||
required: true
|
||||
pull_requests:
|
||||
description: The pull request number.
|
||||
default: ''
|
||||
required: true
|
||||
skipSlackPing:
|
||||
description: 'Skip Slack Ping: If true, the Slack ping will be skipped (useful for testing)'
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
slackChannelOverride:
|
||||
description: 'Slack Channel Override: The channel ID to send the Slack ping about the code freeze violation'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
GIT_COMMITTER_NAME: 'WooCommerce Bot'
|
||||
|
@ -31,320 +31,321 @@ env:
|
|||
GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com'
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
run: ${{ steps.check.outputs.run }}
|
||||
steps:
|
||||
- name: check
|
||||
id: check
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let run = false;
|
||||
verify:
|
||||
name: Verify
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
run: ${{ steps.check.outputs.run }}
|
||||
steps:
|
||||
- name: check
|
||||
id: check
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let run = false;
|
||||
|
||||
const isManualTrigger = context.payload.inputs && context.payload.inputs.release_branch && context.payload.inputs.release_branch != null;
|
||||
|
||||
const isMergedMilestonedIssue = context.payload.issue && context.payload.issue.pull_request != null && context.payload.issue.pull_request.merged_at != null && context.payload.issue.milestone != null;
|
||||
|
||||
const isMergedMilestonedPR = context.payload.pull_request && context.payload.pull_request != null && context.payload.pull_request.merged == true && context.payload.pull_request.milestone != null;
|
||||
const isManualTrigger = context.payload.inputs && context.payload.inputs.release_branch && context.payload.inputs.release_branch != null;
|
||||
|
||||
const isBot = context.payload.pull_request && ( context.payload.pull_request.user.login == 'github-actions[bot]' || context.payload.pull_request.user.type == 'Bot' );
|
||||
const isMergedMilestonedIssue = context.payload.issue && context.payload.issue.pull_request != null && context.payload.issue.pull_request.merged_at != null && context.payload.issue.milestone != null;
|
||||
|
||||
if ( !isBot && ( isManualTrigger || isMergedMilestonedIssue || isMergedMilestonedPR ) ) {
|
||||
console.log( "::set-output name=run::true" );
|
||||
} else {
|
||||
console.log( "::set-output name=run::false" );
|
||||
}
|
||||
prep:
|
||||
name: Prep inputs
|
||||
runs-on: ubuntu-20.04
|
||||
needs: verify
|
||||
if: needs.verify.outputs.run == 'true'
|
||||
outputs:
|
||||
release: ${{ steps.prep-inputs.outputs.release }}
|
||||
pr: ${{ steps.prep-inputs.outputs.pr }}
|
||||
version: ${{ steps.prep-inputs.outputs.version }}
|
||||
steps:
|
||||
- name: Prep inputs
|
||||
id: prep-inputs
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const event = ${{ toJSON( github.event ) }}
|
||||
const isMergedMilestonedPR = context.payload.pull_request && context.payload.pull_request != null && context.payload.pull_request.merged == true && context.payload.pull_request.milestone != null;
|
||||
|
||||
// Means this workflow was triggered manually.
|
||||
if ( event.inputs && event.inputs.release_branch ) {
|
||||
const releaseBranch = '${{ inputs.release_branch }}'
|
||||
const version = releaseBranch.replace( 'release/', '' );
|
||||
const isBot = context.payload.pull_request && ( context.payload.pull_request.user.login == 'github-actions[bot]' || context.payload.pull_request.user.type == 'Bot' );
|
||||
|
||||
console.log( "::set-output name=version::" + version )
|
||||
console.log( "::set-output name=release::${{ inputs.release_branch }}" )
|
||||
} else if ( event.action === 'milestoned' ) {
|
||||
const version = '${{ github.event.issue.milestone.title }}'
|
||||
const release = version.substring( 0, 3 )
|
||||
if ( !isBot && ( isManualTrigger || isMergedMilestonedIssue || isMergedMilestonedPR ) ) {
|
||||
core.setOutput( 'run', 'true' );
|
||||
} else {
|
||||
core.setOutput( 'run', 'false' );
|
||||
}
|
||||
prep:
|
||||
name: Prep inputs
|
||||
runs-on: ubuntu-20.04
|
||||
needs: verify
|
||||
if: needs.verify.outputs.run == 'true'
|
||||
outputs:
|
||||
release: ${{ steps.prep-inputs.outputs.release }}
|
||||
pr: ${{ steps.prep-inputs.outputs.pr }}
|
||||
version: ${{ steps.prep-inputs.outputs.version }}
|
||||
steps:
|
||||
- name: Prep inputs
|
||||
id: prep-inputs
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const event = ${{ toJSON( github.event ) }}
|
||||
|
||||
console.log( "::set-output name=version::" + version )
|
||||
console.log( "::set-output name=release::release/" + release )
|
||||
} else {
|
||||
const version = '${{ github.event.pull_request.milestone.title }}'
|
||||
const release = version.substring( 0, 3 )
|
||||
// Means this workflow was triggered manually.
|
||||
if ( event.inputs && event.inputs.release_branch ) {
|
||||
const releaseBranch = '${{ inputs.release_branch }}'
|
||||
const version = releaseBranch.replace( 'release/', '' )
|
||||
|
||||
console.log( "::set-output name=version::" + version )
|
||||
console.log( "::set-output name=release::release/" + release )
|
||||
}
|
||||
core.setOutput( 'version', version )
|
||||
core.setOutput( 'release', releaseBranch )
|
||||
} else if ( event.action === 'milestoned' ) {
|
||||
const version = '${{ github.event.issue.milestone.title }}'
|
||||
const release = version.substring( 0, 3 )
|
||||
|
||||
// Means this workflow was triggered manually.
|
||||
if ( event.inputs && event.inputs.pull_requests ) {
|
||||
console.log( "::set-output name=pr::${{ inputs.pull_requests }}" )
|
||||
} else if ( event.action === 'milestoned' ) {
|
||||
console.log( "::set-output name=pr::${{ github.event.issue.number }}" )
|
||||
} else {
|
||||
console.log( "::set-output name=pr::${{ github.event.pull_request.number }}" )
|
||||
}
|
||||
check-release-branch-exists:
|
||||
name: Check for existence of release branch
|
||||
runs-on: ubuntu-20.04
|
||||
needs: prep
|
||||
steps:
|
||||
- name: Check for release branch
|
||||
id: release-breanch-check
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
// This will throw an error for non-200 responses, which prevents subsequent jobs from completing, as desired.
|
||||
await github.request( 'GET /repos/{owner}/{repo}/branches/{branch}', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
branch: '${{ needs.prep.outputs.release }}',
|
||||
} );
|
||||
cherry-pick-run:
|
||||
name: Run cherry pick tool
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [ prep, check-release-branch-exists ]
|
||||
if: success()
|
||||
steps:
|
||||
- name: Checkout release branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
core.setOutput( 'version', version )
|
||||
core.setOutput( 'release', `release/${release}` )
|
||||
} else {
|
||||
const version = '${{ github.event.pull_request.milestone.title }}'
|
||||
const release = version.substring( 0, 3 )
|
||||
|
||||
- name: Git fetch the release branch
|
||||
run: git fetch origin ${{ needs.prep.outputs.release }}
|
||||
|
||||
- name: Checkout release branch
|
||||
run: git checkout ${{ needs.prep.outputs.release }}
|
||||
|
||||
- name: Create a cherry pick branch based on release branch
|
||||
run: git checkout -b cherry-pick-${{ needs.prep.outputs.version }}/${{ needs.prep.outputs.pr }}
|
||||
|
||||
- name: Get commit sha from PR
|
||||
id: commit-sha
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: '${{ needs.prep.outputs.pr }}'
|
||||
})
|
||||
|
||||
console.log( `::set-output name=sha::${ pr.data.merge_commit_sha }` )
|
||||
|
||||
- name: Cherry pick
|
||||
run: |
|
||||
git cherry-pick ${{ steps.commit-sha.outputs.sha }}
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const fs = require( 'node:fs' );
|
||||
|
||||
const changelogsToBeDeleted = []
|
||||
let changelogTxt = '';
|
||||
|
||||
const commit = await github.rest.repos.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: '${{ steps.commit-sha.outputs.sha }}'
|
||||
})
|
||||
|
||||
for ( const file of commit.data.files ) {
|
||||
if ( file.filename.match( 'plugins/woocommerce/changelog/' ) ) {
|
||||
if ( changelogsToBeDeleted.indexOf( file.filename ) === -1 ) {
|
||||
changelogsToBeDeleted.push( file.filename );
|
||||
}
|
||||
|
||||
let changelogEntry = '';
|
||||
let changelogEntryType = '';
|
||||
|
||||
fs.readFile( './' + file.filename, 'utf-8', function( err, data ) {
|
||||
if ( err ) {
|
||||
console.error( err );
|
||||
}
|
||||
|
||||
const changelogEntryArr = data.split( "\n" );
|
||||
changelogEntryType = data.match( /Type: (.+)/i );
|
||||
changelogEntryType = changelogEntryType[ 1 ].charAt( 0 ).toUpperCase() + changelogEntryType[ 1 ].slice( 1 );
|
||||
|
||||
changelogEntry = changelogEntryArr.filter( el => {
|
||||
return el !== null && typeof el !== 'undefined' && el !== '';
|
||||
} );
|
||||
changelogEntry = changelogEntry[ changelogEntry.length - 1 ];
|
||||
|
||||
// Check if changelogEntry is what we want.
|
||||
if ( changelogEntry.length < 1 ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( changelogEntry.match( /significance:/i ) ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( changelogEntry.match( /type:/i ) ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( changelogEntry.match( /comment:/i ) ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( ! changelogEntry ) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) {
|
||||
if ( err ) {
|
||||
console.error( err );
|
||||
}
|
||||
|
||||
changelogTxt = data.split( "\n" );
|
||||
let isInRange = false;
|
||||
let newChangelogTxt = [];
|
||||
|
||||
for ( const line of changelogTxt ) {
|
||||
if ( isInRange === false && line === '== Changelog ==' ) {
|
||||
isInRange = true;
|
||||
core.setOutput( 'version', version )
|
||||
core.setOutput( 'release', `release/${release}` )
|
||||
}
|
||||
|
||||
if ( isInRange === true && line.match( /\*\*WooCommerce Blocks/ ) ) {
|
||||
isInRange = false;
|
||||
// Means this workflow was triggered manually.
|
||||
if ( event.inputs && event.inputs.pull_requests ) {
|
||||
core.setOutput( 'pr', '${{ inputs.pull_requests }}' )
|
||||
} else if ( event.action === 'milestoned' ) {
|
||||
core.setOutput( 'pr', '${{ github.event.issue.number }}' )
|
||||
} else {
|
||||
core.setOutput( 'pr', '${{ github.event.pull_request.number }}' )
|
||||
}
|
||||
check-release-branch-exists:
|
||||
name: Check for existence of release branch
|
||||
runs-on: ubuntu-20.04
|
||||
needs: prep
|
||||
steps:
|
||||
- name: Check for release branch
|
||||
id: release-breanch-check
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
// This will throw an error for non-200 responses, which prevents subsequent jobs from completing, as desired.
|
||||
await github.request( 'GET /repos/{owner}/{repo}/branches/{branch}', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
branch: '${{ needs.prep.outputs.release }}',
|
||||
} );
|
||||
cherry-pick-run:
|
||||
name: Run cherry pick tool
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [prep, check-release-branch-exists]
|
||||
if: success()
|
||||
steps:
|
||||
- name: Checkout release branch
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Git fetch the release branch
|
||||
run: git fetch origin ${{ needs.prep.outputs.release }}
|
||||
|
||||
- name: Checkout release branch
|
||||
run: git checkout ${{ needs.prep.outputs.release }}
|
||||
|
||||
- name: Create a cherry pick branch based on release branch
|
||||
run: git checkout -b cherry-pick-${{ needs.prep.outputs.version }}/${{ needs.prep.outputs.pr }}
|
||||
|
||||
- name: Get commit sha from PR
|
||||
id: commit-sha
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: '${{ needs.prep.outputs.pr }}'
|
||||
})
|
||||
|
||||
core.setOutput( 'sha', pr.data.merge_commit_sha )
|
||||
|
||||
- name: Cherry pick
|
||||
run: |
|
||||
git cherry-pick ${{ steps.commit-sha.outputs.sha }} -m1
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const fs = require( 'node:fs' );
|
||||
|
||||
const changelogsToBeDeleted = []
|
||||
let changelogTxt = '';
|
||||
|
||||
const commit = await github.rest.repos.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: '${{ steps.commit-sha.outputs.sha }}'
|
||||
})
|
||||
|
||||
for ( const file of commit.data.files ) {
|
||||
if ( file.filename.match( 'plugins/woocommerce/changelog/' ) ) {
|
||||
if ( changelogsToBeDeleted.indexOf( file.filename ) === -1 ) {
|
||||
changelogsToBeDeleted.push( file.filename );
|
||||
}
|
||||
|
||||
let changelogEntry = '';
|
||||
let changelogEntryType = '';
|
||||
|
||||
fs.readFile( './' + file.filename, 'utf-8', function( err, data ) {
|
||||
if ( err ) {
|
||||
console.error( err );
|
||||
}
|
||||
|
||||
const changelogEntryArr = data.split( "\n" );
|
||||
changelogEntryType = data.match( /Type: (.+)/i );
|
||||
changelogEntryType = changelogEntryType[ 1 ].charAt( 0 ).toUpperCase() + changelogEntryType[ 1 ].slice( 1 );
|
||||
|
||||
changelogEntry = changelogEntryArr.filter( el => {
|
||||
return el !== null && typeof el !== 'undefined' && el !== '';
|
||||
} );
|
||||
changelogEntry = changelogEntry[ changelogEntry.length - 1 ];
|
||||
|
||||
// Check if changelogEntry is what we want.
|
||||
if ( changelogEntry.length < 1 ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( changelogEntry.match( /significance:/i ) ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( changelogEntry.match( /type:/i ) ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( changelogEntry.match( /comment:/i ) ) {
|
||||
changelogEntry = false;
|
||||
}
|
||||
|
||||
if ( ! changelogEntry ) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) {
|
||||
if ( err ) {
|
||||
console.error( err );
|
||||
}
|
||||
|
||||
changelogTxt = data.split( "\n" );
|
||||
let isInRange = false;
|
||||
let newChangelogTxt = [];
|
||||
|
||||
for ( const line of changelogTxt ) {
|
||||
if ( isInRange === false && line === '== Changelog ==' ) {
|
||||
isInRange = true;
|
||||
}
|
||||
|
||||
if ( isInRange === true && line.match( /\*\*WooCommerce Blocks/ ) ) {
|
||||
isInRange = false;
|
||||
}
|
||||
|
||||
// Find the first match of the entry "Type".
|
||||
if ( isInRange && line.match( `\\* ${changelogEntryType} -` ) ) {
|
||||
newChangelogTxt.push( '* ' + changelogEntryType + ' - ' + changelogEntry + ` [#${{ needs.prep.outputs.pr }}](https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }})` );
|
||||
newChangelogTxt.push( line );
|
||||
isInRange = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
newChangelogTxt.push( line );
|
||||
}
|
||||
|
||||
fs.writeFile( './plugins/woocommerce/readme.txt', newChangelogTxt.join( "\n" ), err => {
|
||||
if ( err ) {
|
||||
console.error( `Unable to generate the changelog entry for PR ${{ needs.prep.outputs.pr }}` );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
// Find the first match of the entry "Type".
|
||||
if ( isInRange && line.match( `\\* ${changelogEntryType} -` ) ) {
|
||||
newChangelogTxt.push( '* ' + changelogEntryType + ' - ' + changelogEntry + ` [#${{ needs.prep.outputs.pr }}](https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }})` );
|
||||
newChangelogTxt.push( line );
|
||||
isInRange = false;
|
||||
continue;
|
||||
}
|
||||
core.setOutput( 'changelogsToBeDeleted', changelogsToBeDeleted.join( ' ' ) )
|
||||
|
||||
newChangelogTxt.push( line );
|
||||
}
|
||||
- name: Delete changelog files from cherry pick branch
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git rm ${{ steps.changelog.outputs.changelogsToBeDeleted }}
|
||||
|
||||
fs.writeFile( './plugins/woocommerce/readme.txt', newChangelogTxt.join( "\n" ), err => {
|
||||
if ( err ) {
|
||||
console.error( `Unable to generate the changelog entry for PR ${{ needs.prep.outputs.pr }}` );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
}
|
||||
- name: Commit changes for cherry pick
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git commit --no-verify -am "Prep for cherry pick ${{ needs.prep.outputs.pr }}"
|
||||
|
||||
console.log( `::set-output name=changelogsToBeDeleted::${ changelogsToBeDeleted }` )
|
||||
- name: Push cherry pick branch up
|
||||
run: git push origin cherry-pick-${{ needs.prep.outputs.version }}/${{ needs.prep.outputs.pr }}
|
||||
|
||||
- name: Delete changelog files from cherry pick branch
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git rm ${{ steps.changelog.outputs.changelogsToBeDeleted }}
|
||||
- name: Create the PR for cherry pick branch
|
||||
id: cherry-pick-pr
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let cherryPickPRBody = "This PR cherry-picks the following PRs into the release branch:\n";
|
||||
|
||||
- name: Commit changes for cherry pick
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git commit --no-verify -am "Prep for cherry pick ${{ needs.prep.outputs.pr }}"
|
||||
cherryPickPRBody = `${cherryPickPRBody}` + `* #${{ needs.prep.outputs.pr }}` + "\n";
|
||||
|
||||
- name: Push cherry pick branch up
|
||||
run: git push origin cherry-pick-${{ needs.prep.outputs.version }}/${{ needs.prep.outputs.pr }}
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: "Cherry pick ${{ needs.prep.outputs.pr }} into ${{ needs.prep.outputs.release }}",
|
||||
head: "cherry-pick-${{ needs.prep.outputs.version }}/${{ needs.prep.outputs.pr }}",
|
||||
base: "${{ needs.prep.outputs.release }}",
|
||||
body: cherryPickPRBody
|
||||
})
|
||||
|
||||
- name: Create the PR for cherry pick branch
|
||||
id: cherry-pick-pr
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let cherryPickPRBody = "This PR cherry-picks the following PRs into the release branch:\n";
|
||||
core.setOutput( 'cherry-pick-pr', pr.data.html_url )
|
||||
|
||||
cherryPickPRBody = `${cherryPickPRBody}` + `* #${{ needs.prep.outputs.pr }}` + "\n";
|
||||
- name: Checkout trunk branch
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git checkout trunk
|
||||
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: "Cherry pick ${{ needs.prep.outputs.pr }} into ${{ needs.prep.outputs.release }}",
|
||||
head: "cherry-pick-${{ needs.prep.outputs.version }}/${{ needs.prep.outputs.pr }}",
|
||||
base: "${{ needs.prep.outputs.release }}",
|
||||
body: cherryPickPRBody
|
||||
})
|
||||
- name: Create a branch based on trunk branch
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git checkout -b delete-changelogs/${{ needs.prep.outputs.pr }}
|
||||
|
||||
console.log( `::set-output name=cherry-pick-pr::${ pr.data.html_url }` )
|
||||
- name: Checkout trunk branch
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git checkout trunk
|
||||
- name: Delete changelogs from trunk
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git rm ${{ steps.changelog.outputs.changelogsToBeDeleted }}
|
||||
|
||||
- name: Create a branch based on trunk branch
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git checkout -b delete-changelogs/${{ needs.prep.outputs.pr }}
|
||||
- name: Commit changes for deletion
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git commit --no-verify -am "Delete changelog files for ${{ needs.prep.outputs.pr }}"
|
||||
|
||||
- name: Delete changelogs from trunk
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git rm ${{ steps.changelog.outputs.changelogsToBeDeleted }}
|
||||
- name: Push deletion branch up
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git push origin delete-changelogs/${{ needs.prep.outputs.pr }}
|
||||
|
||||
- name: Commit changes for deletion
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git commit --no-verify -am "Delete changelog files for ${{ needs.prep.outputs.pr }}"
|
||||
- name: Create the PR for deletion branch
|
||||
id: deletion-pr
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: "Delete changelog files based on PR ${{ needs.prep.outputs.pr }}",
|
||||
head: "delete-changelogs/${{ needs.prep.outputs.pr }}",
|
||||
base: "trunk",
|
||||
body: "Delete changelog files based on PR #${{ needs.prep.outputs.pr }}"
|
||||
})
|
||||
|
||||
- name: Push deletion branch up
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
run: git push origin delete-changelogs/${{ needs.prep.outputs.pr }}
|
||||
core.setOutput( 'deletion-pr', pr.data.html_url )
|
||||
|
||||
- name: Create the PR for deletion branch
|
||||
id: deletion-pr
|
||||
if: steps.changelog.outputs.changelogsToBeDeleted != '' && steps.changelog.outputs.changelogsToBeDeleted != null
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: "Delete changelog files based on PR ${{ needs.prep.outputs.pr }}",
|
||||
head: "delete-changelogs/${{ needs.prep.outputs.pr }}",
|
||||
base: "trunk",
|
||||
body: "Delete changelog files based on PR #${{ needs.prep.outputs.pr }}"
|
||||
})
|
||||
- name: Notify Slack on failure
|
||||
if: ${{ failure() && inputs.skipSlackPing != true }}
|
||||
uses: archive/github-actions-slack@v2.0.0
|
||||
with:
|
||||
slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }}
|
||||
slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}
|
||||
slack-text: |
|
||||
:warning-8c: Code freeze violation. PR(s) created that breaks the Code Freeze for '${{ needs.prep.outputs.release }}' :ice_cube:
|
||||
|
||||
console.log( `::set-output name=deletion-pr::${ pr.data.html_url }` )
|
||||
An attempt to cherry pick PR(s) into outgoing release '${{ needs.prep.outputs.release }}' has failed. This could be due to a merge conflict or something else that requires manual attention. Please check: https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }}
|
||||
|
||||
- name: Notify Slack on failure
|
||||
if: ${{ failure() && inputs.skipSlackPing != true }}
|
||||
uses: archive/github-actions-slack@v2.0.0
|
||||
with:
|
||||
slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }}
|
||||
slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}
|
||||
slack-text: |
|
||||
:warning-8c: Code freeze violation. PR(s) created that breaks the Code Freeze for '${{ needs.prep.outputs.release }}' :ice_cube:
|
||||
- name: Notify Slack on success
|
||||
if: ${{ success() && inputs.skipSlackPing != true }}
|
||||
uses: archive/github-actions-slack@v2.0.0
|
||||
with:
|
||||
slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }}
|
||||
slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}
|
||||
slack-text: |
|
||||
:warning-8c: Code freeze violation. PR(s) created that breaks the Code Freeze for '${{ needs.prep.outputs.release }}' :ice_cube:
|
||||
|
||||
An attempt to cherry pick PR(s) into outgoing release '${{ needs.prep.outputs.release }}' has failed. This could be due to a merge conflict or something else that requires manual attention. Please check: https://github.com/woocommerce/woocommerce/pull/${{ needs.prep.outputs.pr }}
|
||||
Release lead please review:
|
||||
|
||||
- name: Notify Slack on success
|
||||
if: ${{ success() && inputs.skipSlackPing != true }}
|
||||
uses: archive/github-actions-slack@v2.0.0
|
||||
with:
|
||||
slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }}
|
||||
slack-channel: ${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}
|
||||
slack-text: |
|
||||
:warning-8c: Code freeze violation. PR(s) created that breaks the Code Freeze for '${{ needs.prep.outputs.release }}' :ice_cube:
|
||||
|
||||
Release lead please review:
|
||||
|
||||
${{ steps.cherry-pick-pr.outputs.cherry-pick-pr }}
|
||||
${{ steps.deletion-pr.outputs.deletion-pr }}
|
||||
${{ steps.cherry-pick-pr.outputs.cherry-pick-pr }}
|
||||
${{ steps.deletion-pr.outputs.deletion-pr }}
|
||||
|
|
|
@ -1,38 +1,40 @@
|
|||
name: Add Community Label
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
verify:
|
||||
name: Verify
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
|
||||
|
||||
- name: Install Octokit
|
||||
run: npm --prefix .github/workflows/scripts install @octokit/action
|
||||
- name: Install Octokit
|
||||
run: npm --prefix .github/workflows/scripts install @octokit/action
|
||||
|
||||
- name: Check if user is a community contributor
|
||||
id: check
|
||||
run: node .github/workflows/scripts/is-community-contributor.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "If community PR, assign a reviewer"
|
||||
if: github.event.pull_request && steps.check.outputs.is-community == 'yes'
|
||||
uses: shufo/auto-assign-reviewer-by-files@f5f3db9ef06bd72ab6978996988c6462cbdaabf6
|
||||
with:
|
||||
config: ".github/project-community-pr-assigner.yml"
|
||||
token: ${{ secrets.PR_ASSIGN_TOKEN }}
|
||||
- name: Install Actions Core
|
||||
run: npm --prefix .github/workflows/scripts install @actions/core
|
||||
|
||||
- name: Check if user is a community contributor
|
||||
id: check
|
||||
run: node .github/workflows/scripts/is-community-contributor.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 'If community PR, assign a reviewer'
|
||||
if: github.event.pull_request && steps.check.outputs.is-community == 'yes'
|
||||
uses: shufo/auto-assign-reviewer-by-files@f5f3db9ef06bd72ab6978996988c6462cbdaabf6
|
||||
with:
|
||||
config: '.github/project-community-pr-assigner.yml'
|
||||
token: ${{ secrets.PR_ASSIGN_TOKEN }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: Run post release processes
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
env:
|
||||
GIT_COMMITTER_NAME: 'WooCommerce Bot'
|
||||
|
@ -10,119 +10,119 @@ env:
|
|||
GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com'
|
||||
|
||||
jobs:
|
||||
changelog-version-update:
|
||||
name: Update changelog and version
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
changelog-version-update:
|
||||
name: Update changelog and version
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Git fetch trunk branch
|
||||
run: git fetch origin trunk
|
||||
- name: Git fetch trunk branch
|
||||
run: git fetch origin trunk
|
||||
|
||||
- name: Copy readme.txt to vm root
|
||||
run: cp ./plugins/woocommerce/readme.txt ../../readme.txt
|
||||
- name: Copy readme.txt to vm root
|
||||
run: cp ./plugins/woocommerce/readme.txt ../../readme.txt
|
||||
|
||||
- name: Switch to trunk branch
|
||||
run: git checkout trunk
|
||||
- name: Switch to trunk branch
|
||||
run: git checkout trunk
|
||||
|
||||
- name: Create a new branch based on trunk
|
||||
run: git checkout -b prep/post-release-tasks-${{ github.event.release.tag_name }}
|
||||
- name: Create a new branch based on trunk
|
||||
run: git checkout -b prep/post-release-tasks-${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Check if we need to continue processing
|
||||
uses: actions/github-script@v6
|
||||
id: check
|
||||
with:
|
||||
script: |
|
||||
const fs = require( 'node:fs' );
|
||||
const version = ${{ toJSON( github.event.release.tag_name ) }}
|
||||
- name: Check if we need to continue processing
|
||||
uses: actions/github-script@v6
|
||||
id: check
|
||||
with:
|
||||
script: |
|
||||
const fs = require( 'node:fs' );
|
||||
const version = ${{ toJSON( github.event.release.tag_name ) }}
|
||||
|
||||
fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) {
|
||||
if ( err ) {
|
||||
console.error( err );
|
||||
}
|
||||
fs.readFile( './plugins/woocommerce/readme.txt', 'utf-8', function( err, data ) {
|
||||
if ( err ) {
|
||||
console.error( err );
|
||||
}
|
||||
|
||||
const regex = /Stable\stag:\s(\d+\.\d+\.\d+)/;
|
||||
const regex = /Stable\stag:\s(\d+\.\d+\.\d+)/;
|
||||
|
||||
const stableVersion = data.match( regex )[1];
|
||||
const stableVersion = data.match( regex )[1];
|
||||
|
||||
// If the release version is less than stable version we can bail.
|
||||
if ( version.localeCompare( stableVersion, undefined, { numeric: true, sensitivity: 'base' } ) == -1 ) {
|
||||
console.log( 'Release version is less than stable version. No automated action taken. A manual process is required.' );
|
||||
console.log( `::set-output name=continue::false` )
|
||||
return;
|
||||
} else {
|
||||
console.log( `::set-output name=continue::true` )
|
||||
}
|
||||
} )
|
||||
// If the release version is less than stable version we can bail.
|
||||
if ( version.localeCompare( stableVersion, undefined, { numeric: true, sensitivity: 'base' } ) == -1 ) {
|
||||
console.log( 'Release version is less than stable version. No automated action taken. A manual process is required.' );
|
||||
core.setOutput( 'continue', 'false' )
|
||||
return;
|
||||
} else {
|
||||
core.setOutput( 'continue', 'true' )
|
||||
}
|
||||
} )
|
||||
|
||||
- name: Update changelog.txt entries
|
||||
uses: actions/github-script@v6
|
||||
id: update-entries
|
||||
if: steps.check.outputs.continue == 'true'
|
||||
with:
|
||||
script: |
|
||||
const fs = require( 'node:fs' );
|
||||
const version = ${{ toJSON( github.event.release.tag_name ) }}
|
||||
- name: Update changelog.txt entries
|
||||
uses: actions/github-script@v6
|
||||
id: update-entries
|
||||
if: steps.check.outputs.continue == 'true'
|
||||
with:
|
||||
script: |
|
||||
const fs = require( 'node:fs' );
|
||||
const version = ${{ toJSON( github.event.release.tag_name ) }}
|
||||
|
||||
// Read the saved readme.txt file from earlier.
|
||||
fs.readFile( '../../readme.txt', 'utf-8', function( err, readme ) {
|
||||
if ( err ) {
|
||||
console.log( `::set-output name=continue::false` )
|
||||
console.error( err );
|
||||
}
|
||||
// Read the saved readme.txt file from earlier.
|
||||
fs.readFile( '../../readme.txt', 'utf-8', function( err, readme ) {
|
||||
if ( err ) {
|
||||
core.setOutput( 'continue', 'false' );
|
||||
console.error( err );
|
||||
}
|
||||
|
||||
const regex = /(== Changelog ==[\s\S]+)\s{2}\[See changelog for all versions\]\(https:\/\/raw\.githubusercontent\.com\/woocommerce\/woocommerce\/trunk\/changelog\.txt\)\./;
|
||||
const regex = /(== Changelog ==[\s\S]+)\s{2}\[See changelog for all versions\]\(https:\/\/raw\.githubusercontent\.com\/woocommerce\/woocommerce\/trunk\/changelog\.txt\)\./;
|
||||
|
||||
const entries = readme.match( regex )[1];
|
||||
const entries = readme.match( regex )[1];
|
||||
|
||||
fs.readFile( './changelog.txt', 'utf-8', function( err, changelog ) {
|
||||
if ( err ) {
|
||||
console.log( `::set-output name=continue::false` )
|
||||
console.error( err );
|
||||
}
|
||||
fs.readFile( './changelog.txt', 'utf-8', function( err, changelog ) {
|
||||
if ( err ) {
|
||||
core.setOutput( 'continue', 'false' );
|
||||
console.error( err );
|
||||
}
|
||||
|
||||
const regex = /== Changelog ==/;
|
||||
const regex = /== Changelog ==/;
|
||||
|
||||
const updatedChangelog = changelog.replace( regex, entries );
|
||||
const updatedChangelog = changelog.replace( regex, entries );
|
||||
|
||||
fs.writeFile( './changelog.txt', updatedChangelog, err => {
|
||||
if ( err ) {
|
||||
console.log( `::set-output name=continue::false` )
|
||||
console.error( 'Unable to update changelog entries in changelog.txt' );
|
||||
}
|
||||
fs.writeFile( './changelog.txt', updatedChangelog, err => {
|
||||
if ( err ) {
|
||||
core.setOutput( 'continue', 'false' );
|
||||
console.error( 'Unable to update changelog entries in changelog.txt' );
|
||||
}
|
||||
|
||||
console.log( `::set-output name=continue::true` )
|
||||
} )
|
||||
} )
|
||||
} )
|
||||
core.setOutput( 'continue', 'true' );
|
||||
} )
|
||||
} )
|
||||
} )
|
||||
|
||||
- name: Commit changes
|
||||
if: steps.update-entries.outputs.continue == 'true'
|
||||
run: git commit -am "Prep trunk post release ${{ github.event.release.tag_name }}"
|
||||
- name: Commit changes
|
||||
if: steps.update-entries.outputs.continue == 'true'
|
||||
run: git commit -am "Prep trunk post release ${{ github.event.release.tag_name }}"
|
||||
|
||||
- name: Push branch up
|
||||
if: steps.update-entries.outputs.continue == 'true'
|
||||
run: git push origin prep/post-release-tasks-${{ github.event.release.tag_name }}
|
||||
- name: Push branch up
|
||||
if: steps.update-entries.outputs.continue == 'true'
|
||||
run: git push origin prep/post-release-tasks-${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Create the PR
|
||||
if: steps.update-entries.outputs.continue == 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const body = "This PR updates the changelog.txt entries based on the latest release: ${{ github.event.release.tag_name }}"
|
||||
- name: Create the PR
|
||||
if: steps.update-entries.outputs.continue == 'true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const body = "This PR updates the changelog.txt entries based on the latest release: ${{ github.event.release.tag_name }}"
|
||||
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: "Update changelog.txt from release ${{ github.event.release.tag_name }}",
|
||||
head: "prep/post-release-tasks-${{ github.event.release.tag_name }}",
|
||||
base: "trunk",
|
||||
body: body
|
||||
})
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: "Update changelog.txt from release ${{ github.event.release.tag_name }}",
|
||||
head: "prep/post-release-tasks-${{ github.event.release.tag_name }}",
|
||||
base: "trunk",
|
||||
body: body
|
||||
})
|
||||
|
||||
const prCreated = await github.rest.pulls.requestReviewers({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.data.number,
|
||||
reviewers: ["${{ github.event.release.author.login }}"]
|
||||
})
|
||||
const prCreated = await github.rest.pulls.requestReviewers({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.data.number,
|
||||
reviewers: ["${{ github.event.release.author.login }}"]
|
||||
})
|
||||
|
|
|
@ -12,8 +12,8 @@ jobs:
|
|||
name: Runs E2E tests.
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-report
|
||||
outputs:
|
||||
E2E_GRAND_TOTAL: ${{ steps.count_e2e_total.outputs.E2E_GRAND_TOTAL }}
|
||||
steps:
|
||||
|
@ -80,8 +80,8 @@ jobs:
|
|||
name: Runs API tests.
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: 'Release: Code freeze'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 16 * * 4' # Run at 1600 UTC on Thursdays.
|
||||
- cron: '0 23 * * 1' # Run at 2300 UTC on Mondays.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
timeOverride:
|
||||
|
@ -42,12 +42,12 @@ jobs:
|
|||
$now = strtotime( getenv( 'TIME_OVERRIDE' ) );
|
||||
}
|
||||
|
||||
// Code freeze comes 26 days prior to release day.
|
||||
$release_time = strtotime( '+26 days', $now );
|
||||
// Code freeze comes 22 days prior to release day.
|
||||
$release_time = strtotime( '+22 days', $now );
|
||||
$release_day_of_week = date( 'l', $release_time );
|
||||
$release_day_of_month = (int) date( 'j', $release_time );
|
||||
|
||||
// If 26 days from now isn't the second Tuesday, then it's not code freeze day.
|
||||
// If 22 days from now isn't the second Tuesday, then it's not code freeze day.
|
||||
if ( 'Tuesday' !== $release_day_of_week || $release_day_of_month < 8 || $release_day_of_month > 14 ) {
|
||||
file_put_contents( getenv( 'GITHUB_OUTPUT' ), "freeze=1\n", FILE_APPEND );
|
||||
} else {
|
||||
|
@ -163,7 +163,7 @@ jobs:
|
|||
workflow_id: 'release-changelog.yml',
|
||||
ref: 'trunk',
|
||||
inputs: {
|
||||
releaseVersion: "release/${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }}",
|
||||
releaseBranch: "${{ needs.maybe-create-next-milestone-and-release-branch.outputs.next_version }}"
|
||||
releaseVersion: "${{ needs.maybe-create-next-milestone-and-release-branch.outputs.release_version }}",
|
||||
releaseBranch: "${{ needs.maybe-create-next-milestone-and-release-branch.outputs.branch }}"
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,50 +1,62 @@
|
|||
// Note you'll need to install this dependency as part of your workflow.
|
||||
const { Octokit } = require('@octokit/action');
|
||||
// Note you'll need to install these dependencies as part of your workflow.
|
||||
const { Octokit } = require( '@octokit/action' );
|
||||
const core = require( '@actions/core' );
|
||||
|
||||
// Note that this script assumes you set GITHUB_TOKEN in env, if you don't
|
||||
// this won't work.
|
||||
const octokit = new Octokit();
|
||||
|
||||
const getIssueAuthor = (payload) => {
|
||||
return payload?.issue?.user?.login || payload?.pull_request?.user?.login || null;
|
||||
}
|
||||
const getIssueAuthor = ( payload ) => {
|
||||
return (
|
||||
payload?.issue?.user?.login ||
|
||||
payload?.pull_request?.user?.login ||
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const isCommunityContributor = async (owner, repo, username) => {
|
||||
if (username) {
|
||||
const {data: {permission}} = await octokit.rest.repos.getCollaboratorPermissionLevel({
|
||||
const isCommunityContributor = async ( owner, repo, username ) => {
|
||||
if ( username ) {
|
||||
const {
|
||||
data: { permission },
|
||||
} = await octokit.rest.repos.getCollaboratorPermissionLevel( {
|
||||
owner,
|
||||
repo,
|
||||
username,
|
||||
});
|
||||
|
||||
} );
|
||||
|
||||
return permission === 'read' || permission === 'none';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const addLabel = async(label, owner, repo, issueNumber) => {
|
||||
await octokit.rest.issues.addLabels({
|
||||
const addLabel = async ( label, owner, repo, issueNumber ) => {
|
||||
await octokit.rest.issues.addLabels( {
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
labels: [label],
|
||||
});
|
||||
}
|
||||
labels: [ label ],
|
||||
} );
|
||||
};
|
||||
|
||||
const applyLabelToCommunityContributor = async () => {
|
||||
const eventPayload = require(process.env.GITHUB_EVENT_PATH);
|
||||
const username = getIssueAuthor(eventPayload);
|
||||
const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/");
|
||||
const eventPayload = require( process.env.GITHUB_EVENT_PATH );
|
||||
const username = getIssueAuthor( eventPayload );
|
||||
const [ owner, repo ] = process.env.GITHUB_REPOSITORY.split( '/' );
|
||||
const { number } = eventPayload?.issue || eventPayload?.pull_request;
|
||||
|
||||
const isCommunityUser = await isCommunityContributor(owner, repo, username);
|
||||
console.log( '::set-output name=is-community::%s', isCommunityUser ? 'yes' : 'no' );
|
||||
|
||||
if (isCommunityUser) {
|
||||
console.log('Adding community contributor label');
|
||||
await addLabel('type: community contribution', owner, repo, number);
|
||||
|
||||
const isCommunityUser = await isCommunityContributor(
|
||||
owner,
|
||||
repo,
|
||||
username
|
||||
);
|
||||
|
||||
core.setOutput( 'is-community', isCommunityUser ? 'yes' : 'no' );
|
||||
|
||||
if ( isCommunityUser ) {
|
||||
console.log( 'Adding community contributor label' );
|
||||
await addLabel( 'type: community contribution', owner, repo, number );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
applyLabelToCommunityContributor();
|
||||
|
|
|
@ -23,14 +23,14 @@ function set_output( $name, $value ) {
|
|||
file_put_contents( getenv( 'GITHUB_OUTPUT' ), "{$name}={$value}" . PHP_EOL, FILE_APPEND );
|
||||
}
|
||||
|
||||
// Code freeze comes 26 days prior to release day.
|
||||
$release_time = strtotime( '+26 days', $now );
|
||||
// Code freeze comes 22 days prior to release day.
|
||||
$release_time = strtotime( '+22 days', $now );
|
||||
$release_day_of_week = date( 'l', $release_time );
|
||||
$release_day_of_month = (int) date( 'j', $release_time );
|
||||
|
||||
// If 26 days from now isn't the second Tuesday, then it's not code freeze day.
|
||||
// If 22 days from now isn't the second Tuesday, then it's not code freeze day.
|
||||
if ( 'Tuesday' !== $release_day_of_week || $release_day_of_month < 8 || $release_day_of_month > 14 ) {
|
||||
echo 'Info: Today is not the Thursday of the code freeze.' . PHP_EOL;
|
||||
echo 'Info: Today is not the Monday of the code freeze.' . PHP_EOL;
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,8 @@ jobs:
|
|||
working-directory: plugins/woocommerce
|
||||
env:
|
||||
E2E_MAX_FAILURES: 25
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/daily.playwright.config.js
|
||||
RESET_SITE: true
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js
|
||||
|
||||
- name: Generate Playwright E2E Test report.
|
||||
if: success() || failure()
|
||||
|
@ -79,8 +80,8 @@ jobs:
|
|||
needs: [e2e-tests]
|
||||
if: success() || failure()
|
||||
env:
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/api-test-report/allure-report
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -163,7 +164,7 @@ jobs:
|
|||
A_PW: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
|
||||
C_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }}
|
||||
C_PW: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
|
||||
P_ID: 274
|
||||
P_ID: 22733
|
||||
run: |
|
||||
./k6 run plugins/woocommerce/tests/performance/tests/gh-action-daily-ext-requests.js
|
||||
|
||||
|
@ -219,7 +220,7 @@ jobs:
|
|||
working-directory: plugins/woocommerce
|
||||
env:
|
||||
E2E_MAX_FAILURES: 15
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/daily.playwright.config.js
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js
|
||||
|
||||
- name: Generate E2E Test report.
|
||||
if: success() || failure()
|
||||
|
|
|
@ -72,8 +72,8 @@ yarn.lock
|
|||
# Editors
|
||||
nbproject/private/
|
||||
|
||||
# Test Results
|
||||
test-results.json
|
||||
# E2E and API Test Results
|
||||
test-results
|
||||
|
||||
# Admin Feature config
|
||||
plugins/woocommerce/includes/react-admin/feature-config.php
|
||||
|
@ -89,13 +89,6 @@ allure-results
|
|||
changes.json
|
||||
.env
|
||||
|
||||
# Playwright output & working files
|
||||
/plugins/woocommerce/tests/e2e-pw/output
|
||||
/plugins/woocommerce/tests/e2e-pw/report
|
||||
/plugins/woocommerce/tests/e2e-pw/storage
|
||||
/plugins/woocommerce/tests/e2e-pw/test-results.json
|
||||
/plugins/woocommerce/tests/api-core-tests/output
|
||||
|
||||
# Turborepo
|
||||
.turbo
|
||||
|
||||
|
|
249
changelog.txt
249
changelog.txt
|
@ -1,5 +1,254 @@
|
|||
== Changelog ==
|
||||
|
||||
= 7.2.2 2022-12-21 =
|
||||
|
||||
** WooCommerce**
|
||||
|
||||
* Fix - Corrects a hard-coded reference to the WP post meta table within the HPOS Migration Helper, that would fail on some sites. [#36100](https://github.com/woocommerce/woocommerce/pull/36100)
|
||||
|
||||
= 7.2.1 2022-12-16 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Update - Include taxes migration in MigrationHelper::migrate_country_states [#35967](https://github.com/woocommerce/woocommerce/pull/35967)
|
||||
|
||||
= 7.2.0 2022-12-14 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Fix - Corrects a hard-coded reference to the WP post meta table within the HPOS Migration Helper, that would fail on some sites. [#36100](https://github.com/woocommerce/woocommerce/pull/36100)
|
||||
* Fix - Drop usage of WP 5.9 function in the product quantity selector template. [#36054](https://github.com/woocommerce/woocommerce/pull/36054)
|
||||
* Fix - Add a data migration for changed New Zealand and Ukraine state codes [#35669](https://github.com/woocommerce/woocommerce/pull/35669)
|
||||
* Fix - Fix error in onboarding wizard when plugin is activated but includes unexpected output. [#35866](https://github.com/woocommerce/woocommerce/pull/35866)
|
||||
* Fix - Increased margin so that overflow modal content doesn't clip header [#35780](https://github.com/woocommerce/woocommerce/pull/35780)
|
||||
* Fix - Added default additional content to emails via filter woocommerce_email_additional_content_. [#35195](https://github.com/woocommerce/woocommerce/pull/35195)
|
||||
* Fix - Corrects the currency symbol for Libyan Dinar (LYD). [#35395](https://github.com/woocommerce/woocommerce/pull/35395)
|
||||
* Fix - Fix 'Invalid payment method' error upon double click on Delete button of Payment methods table [#30884](https://github.com/woocommerce/woocommerce/pull/30884)
|
||||
* Fix - Fix bg color that was not covering the full page [#35476](https://github.com/woocommerce/woocommerce/pull/35476)
|
||||
* Fix - Fix class name for class FirstDownlaodableProduct [#35383](https://github.com/woocommerce/woocommerce/pull/35383)
|
||||
* Fix - Fixed "Unsupported operand types" error. [#34327](https://github.com/woocommerce/woocommerce/pull/34327)
|
||||
* Fix - Fix inconsistent return type of class WC_Shipping_Rate->get_shipping_tax() [#35453](https://github.com/woocommerce/woocommerce/pull/35453)
|
||||
* Fix - Fix invalid wcadmin_install_plugin_error event props [#35411](https://github.com/woocommerce/woocommerce/pull/35411)
|
||||
* Fix - Fix JS error when the business step is accessed directly via URL without completing the previous steps [#35045](https://github.com/woocommerce/woocommerce/pull/35045)
|
||||
* Fix - fix popper position for in-app marketplace tour [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Fix - Fix WooCommerce icons not loading in the site editor. [#35532](https://github.com/woocommerce/woocommerce/pull/35532)
|
||||
* Fix - FQCN for WP_Error in PHPDoc. [#35305](https://github.com/woocommerce/woocommerce/pull/35305)
|
||||
* Fix - Make the user search metabox for orders show the same information for the loaded user and for search results [#35244](https://github.com/woocommerce/woocommerce/pull/35244)
|
||||
* Fix - Override filter_meta_data method, since it should be a no-op anyway. [#35192](https://github.com/woocommerce/woocommerce/pull/35192)
|
||||
* Fix - Remove the direct dependency on `$_POST` when validating checkout data. [#35329](https://github.com/woocommerce/woocommerce/pull/35329)
|
||||
* Fix - Revert change that auto collapses the product short description field. [#35213](https://github.com/woocommerce/woocommerce/pull/35213)
|
||||
* Fix - Skip flaky settings API test [#35338](https://github.com/woocommerce/woocommerce/pull/35338)
|
||||
* Fix - Update Playwright from 1.26.1 to 1.27.1 [#35106](https://github.com/woocommerce/woocommerce/pull/35106)
|
||||
* Fix - When the minimum and maximum quantity are identical, render the quantity input and set it to disabled. [#34282](https://github.com/woocommerce/woocommerce/pull/34282)
|
||||
* Add - Add "Empty Trash" functionality to HPOS list table. [#35489](https://github.com/woocommerce/woocommerce/pull/35489)
|
||||
* Add - Add add attribute modal to the attribute field in the new product management MVP [#34999](https://github.com/woocommerce/woocommerce/pull/34999)
|
||||
* Add - Add add new option for the category dropdown within the product MVP [#35132](https://github.com/woocommerce/woocommerce/pull/35132)
|
||||
* Add - Add contextual product more menu [#35447](https://github.com/woocommerce/woocommerce/pull/35447)
|
||||
* Add - Added a guided tour for WooCommerce Extensions page [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Add - Added npm script for Playwright API Core Tests [#35283](https://github.com/woocommerce/woocommerce/pull/35283)
|
||||
* Add - Added states for Senegal. [#35199](https://github.com/woocommerce/woocommerce/pull/35199)
|
||||
* Add - Added the "Tour the WooCommerce Marketplace" task to onboarding tasks list [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Add - Added Ukrainian subdivisions. [#35493](https://github.com/woocommerce/woocommerce/pull/35493)
|
||||
* Add - Adding attribute edit modal for new product screen. [#35269](https://github.com/woocommerce/woocommerce/pull/35269)
|
||||
* Add - Add manual stock management section to product management experience [#35047](https://github.com/woocommerce/woocommerce/pull/35047)
|
||||
* Add - Add new Category dropdown field to the new Product Management screen. [#34400](https://github.com/woocommerce/woocommerce/pull/34400)
|
||||
* Add - add new track events for in-app marketplace tour [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Add - Add option and modal to create new attribute terms within MVP attribute modal. [#35131](https://github.com/woocommerce/woocommerce/pull/35131)
|
||||
* Add - Add placeholder to description field [#35286](https://github.com/woocommerce/woocommerce/pull/35286)
|
||||
* Add - Add playwright api-core-tests for data crud operations [#35347](https://github.com/woocommerce/woocommerce/pull/35347)
|
||||
* Add - Add playwright api-core-tests for payment gateways crud operations [#35279](https://github.com/woocommerce/woocommerce/pull/35279)
|
||||
* Add - Add playwright api-core-tests for product reviews crud operations [#35163](https://github.com/woocommerce/woocommerce/pull/35163)
|
||||
* Add - Add playwright api-core-tests for product variations crud operations [#35355](https://github.com/woocommerce/woocommerce/pull/35355)
|
||||
* Add - Add playwright api-core-tests for reports crud operations [#35388](https://github.com/woocommerce/woocommerce/pull/35388)
|
||||
* Add - Add playwright api-core-tests for settingss crud operations [#35253](https://github.com/woocommerce/woocommerce/pull/35253)
|
||||
* Add - Add playwright api-core-tests for system status crud operations [#35254](https://github.com/woocommerce/woocommerce/pull/35254)
|
||||
* Add - Add playwright api-core-tests for webhooks crud operations [#35292](https://github.com/woocommerce/woocommerce/pull/35292)
|
||||
* Add - Add Product description title in old editor for clarification. [#35154](https://github.com/woocommerce/woocommerce/pull/35154)
|
||||
* Add - Add product inventory advanced section [#35164](https://github.com/woocommerce/woocommerce/pull/35164)
|
||||
* Add - Add product management description to new product management experience [#34961](https://github.com/woocommerce/woocommerce/pull/34961)
|
||||
* Add - Add product state badge to product form header [#35460](https://github.com/woocommerce/woocommerce/pull/35460)
|
||||
* Add - Add product title to header when available [#35431](https://github.com/woocommerce/woocommerce/pull/35431)
|
||||
* Add - Add scheduled sale support to new product edit page. [#34538](https://github.com/woocommerce/woocommerce/pull/34538)
|
||||
* Add - Adds new Inbox Note to provide more information about WooCommerce Payments to users who dismiss the WCPay promo but say that they want more information in the exit survey. [#35581](https://github.com/woocommerce/woocommerce/pull/35581)
|
||||
* Add - Add summary to new product page experience [#35201](https://github.com/woocommerce/woocommerce/pull/35201)
|
||||
* Add - Include order datastore information in status report. [#35487](https://github.com/woocommerce/woocommerce/pull/35487)
|
||||
* Add - Make it possible to add custom bulk action handling to the admin order list screen (when HPOS is enabled). [#35442](https://github.com/woocommerce/woocommerce/pull/35442)
|
||||
* Add - Set In-App Marketplace Tour as completed on tour close [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Add - When custom order tables are not authoritative, admin UI requests will be redirected to the matching legacy order screen as appropriate. [#35463](https://github.com/woocommerce/woocommerce/pull/35463)
|
||||
* Update - Woo Blocks 8.9.2 [#35805](https://github.com/woocommerce/woocommerce/pull/35805)
|
||||
* Update - Comment: Update WooCommerce Blocks to 8.7.2 [#35101](https://github.com/woocommerce/woocommerce/pull/35101)
|
||||
* Update - Comment: Update WooCommerce Blocks to 8.7.3 [#35219](https://github.com/woocommerce/woocommerce/pull/35219)
|
||||
* Update - Comment: Update WooCommerce Blocks to 8.9.1 [#35564](https://github.com/woocommerce/woocommerce/pull/35564)
|
||||
* Update - CustomOrdersTableController::custom_orders_table_usage_is_enabled returns now false if the HPOS feature is disabled [#35597](https://github.com/woocommerce/woocommerce/pull/35597)
|
||||
* Update - Disable inventory stock toggle when product stock management is disabled [#35059](https://github.com/woocommerce/woocommerce/pull/35059)
|
||||
* Update - Improve the loading time of WooCommerce setup widget for large databases [#35334](https://github.com/woocommerce/woocommerce/pull/35334)
|
||||
* Update - Permit showing a guided tour for WooCommerce Extensions page on desktops only [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Update - Remove adding and managing products note [#35319](https://github.com/woocommerce/woocommerce/pull/35319)
|
||||
* Update - Remove first downloadable product note [#35318](https://github.com/woocommerce/woocommerce/pull/35318)
|
||||
* Update - Remove InsightFirstProductAndPayment note [#35309](https://github.com/woocommerce/woocommerce/pull/35309)
|
||||
* Update - Remove insight on first sale note [#35341](https://github.com/woocommerce/woocommerce/pull/35341)
|
||||
* Update - Remove manage store activity note [#35320](https://github.com/woocommerce/woocommerce/pull/35320)
|
||||
* Update - Remove Popover.Slot usage and make use of exported SelectControlMenuSlot. [#35353](https://github.com/woocommerce/woocommerce/pull/35353)
|
||||
* Update - Remove update store details note [#35322](https://github.com/woocommerce/woocommerce/pull/35322)
|
||||
* Update - Update Array checks in playwright api-core-tests as some of the existing tests would produce false positives [#35462](https://github.com/woocommerce/woocommerce/pull/35462)
|
||||
* Update - Update playwright api-core-tests for shipping crud operations [#35332](https://github.com/woocommerce/woocommerce/pull/35332)
|
||||
* Update - Update playwright api-core-tests to execute for both base test environment and base JN environment with WooCommerce [#35522](https://github.com/woocommerce/woocommerce/pull/35522)
|
||||
* Update - Update products task list UI [#35611](https://github.com/woocommerce/woocommerce/pull/35611)
|
||||
* Update - Update ShippingLabelBanner add_meta_box action to only trigger on shop_order pages and remove deprecated function call. [#35212](https://github.com/woocommerce/woocommerce/pull/35212)
|
||||
* Update - Update WooCommerce Blocks to 8.9.0 [#35521](https://github.com/woocommerce/woocommerce/pull/35521)
|
||||
* Dev - Add variation price shortcut [#34948](https://github.com/woocommerce/woocommerce/pull/34948)
|
||||
* Dev - Cleanup and deprecate unused Task properties and methods [#35450](https://github.com/woocommerce/woocommerce/pull/35450)
|
||||
* Dev - Enable Playwright tests on Daily Smoke Test workflow and upload its Allure reports to S3 bucket. [#35114](https://github.com/woocommerce/woocommerce/pull/35114)
|
||||
* Dev - Move product action buttons to header menu [#35214](https://github.com/woocommerce/woocommerce/pull/35214)
|
||||
* Dev - Revert the changes introduced in PR #35282 [#35337](https://github.com/woocommerce/woocommerce/pull/35337)
|
||||
* Dev - Show a dismissible snackbar if the server responds with an error [#35160](https://github.com/woocommerce/woocommerce/pull/35160)
|
||||
* Dev - Update api-core-tests readme for consistency with new command and updates to other commands too. [#35303](https://github.com/woocommerce/woocommerce/pull/35303)
|
||||
* Dev - Updated the COT plugin URL now that this feature can be enabled in a different way. [#34990](https://github.com/woocommerce/woocommerce/pull/34990)
|
||||
* Dev - Update the list of tags for WC plugin on .org [#35573](https://github.com/woocommerce/woocommerce/pull/35573)
|
||||
* Dev - Update unit test install script for db sockets. [#35152](https://github.com/woocommerce/woocommerce/pull/35152)
|
||||
* Dev - Use plugins/woocommerce/tests/e2e-pw folder for saving test outputs [#35206](https://github.com/woocommerce/woocommerce/pull/35206)
|
||||
* Dev - Uses the globa-setup.js to setup permalinks structure [#35282](https://github.com/woocommerce/woocommerce/pull/35282)
|
||||
* Tweak - Move HPOS hook woocommerce_before_delete_order before deleting order. [#35517](https://github.com/woocommerce/woocommerce/pull/35517)
|
||||
* Tweak - Adds new filter `woocommerce_get_customer_payment_tokens_limit` to set limit on number of payment methods fetched within the My Account page. [#29850](https://github.com/woocommerce/woocommerce/pull/29850)
|
||||
* Tweak - Add source parameter for calls to the subscriptions endpoint on WooCommerce.com [#35051](https://github.com/woocommerce/woocommerce/pull/35051)
|
||||
* Tweak - Fix @version header in form-login.php [#35479](https://github.com/woocommerce/woocommerce/pull/35479)
|
||||
* Tweak - Move HPOS hook woocommerce_before_delete_order before deleting order. [#35517](https://github.com/woocommerce/woocommerce/pull/35517)
|
||||
* Tweak - typo fix [#35111](https://github.com/woocommerce/woocommerce/pull/35111)
|
||||
* Tweak - Unwrap product page input props and pass via getInputProps [#35034](https://github.com/woocommerce/woocommerce/pull/35034)
|
||||
* Tweak - Updates the currency symbol used for the Azerbaijani manat. [#30605](https://github.com/woocommerce/woocommerce/pull/30605)
|
||||
* Tweak - Use new Tooltip component instead of EnrichedLabel [#35024](https://github.com/woocommerce/woocommerce/pull/35024)
|
||||
* Enhancement - Change the product info section title to Product Details [#35255](https://github.com/woocommerce/woocommerce/pull/35255)
|
||||
* Enhancement - Fix the display of letter descenders in the shipping class dropdown menu [#35258](https://github.com/woocommerce/woocommerce/pull/35258)
|
||||
* Enhancement - Improve the communication around required and optional [#35266](https://github.com/woocommerce/woocommerce/pull/35266)
|
||||
* Enhancement - Increase the spacing between the shipping box illustration and the dimensions fields [#35259](https://github.com/woocommerce/woocommerce/pull/35259)
|
||||
* Enhancement - Optimize query usage in the Onboarding tasks [#35065](https://github.com/woocommerce/woocommerce/pull/35065)
|
||||
* Enhancement - Remove some placeholder values [#35267](https://github.com/woocommerce/woocommerce/pull/35267)
|
||||
* Enhancement - Replace the trash can icon in the attribute list [#35133](https://github.com/woocommerce/woocommerce/pull/35133)
|
||||
* Enhancement - Select the current new added shipping class [#35123](https://github.com/woocommerce/woocommerce/pull/35123)
|
||||
* Enhancement - Tweaks the PR template for GitHub pull requests [#34597](https://github.com/woocommerce/woocommerce/pull/34597)
|
||||
|
||||
|
||||
= 7.2.1 2022-12-16 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Update - Include taxes migration in MigrationHelper::migrate_country_states [#35967](https://github.com/woocommerce/woocommerce/pull/35967)
|
||||
|
||||
= 7.2.0 2022-12-14 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Fix - Drop usage of WP 5.9 function in the product quantity selector template. [#36054](https://github.com/woocommerce/woocommerce/pull/36054)
|
||||
* Fix - Add a data migration for changed New Zealand and Ukraine state codes [#35669](https://github.com/woocommerce/woocommerce/pull/35669)
|
||||
* Fix - Fix error in onboarding wizard when plugin is activated but includes unexpected output. [#35866](https://github.com/woocommerce/woocommerce/pull/35866)
|
||||
* Fix - Increased margin so that overflow modal content doesn't clip header [#35780](https://github.com/woocommerce/woocommerce/pull/35780)
|
||||
* Fix - Added default additional content to emails via filter woocommerce_email_additional_content_. [#35195](https://github.com/woocommerce/woocommerce/pull/35195)
|
||||
* Fix - Corrects the currency symbol for Libyan Dinar (LYD). [#35395](https://github.com/woocommerce/woocommerce/pull/35395)
|
||||
* Fix - Fix 'Invalid payment method' error upon double click on Delete button of Payment methods table [#30884](https://github.com/woocommerce/woocommerce/pull/30884)
|
||||
* Fix - Fix bg color that was not covering the full page [#35476](https://github.com/woocommerce/woocommerce/pull/35476)
|
||||
* Fix - Fix class name for class FirstDownlaodableProduct [#35383](https://github.com/woocommerce/woocommerce/pull/35383)
|
||||
* Fix - Fixed "Unsupported operand types" error. [#34327](https://github.com/woocommerce/woocommerce/pull/34327)
|
||||
* Fix - Fix inconsistent return type of class WC_Shipping_Rate->get_shipping_tax() [#35453](https://github.com/woocommerce/woocommerce/pull/35453)
|
||||
* Fix - Fix invalid wcadmin_install_plugin_error event props [#35411](https://github.com/woocommerce/woocommerce/pull/35411)
|
||||
* Fix - Fix JS error when the business step is accessed directly via URL without completing the previous steps [#35045](https://github.com/woocommerce/woocommerce/pull/35045)
|
||||
* Fix - fix popper position for in-app marketplace tour [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Fix - Fix WooCommerce icons not loading in the site editor. [#35532](https://github.com/woocommerce/woocommerce/pull/35532)
|
||||
* Fix - FQCN for WP_Error in PHPDoc. [#35305](https://github.com/woocommerce/woocommerce/pull/35305)
|
||||
* Fix - Make the user search metabox for orders show the same information for the loaded user and for search results [#35244](https://github.com/woocommerce/woocommerce/pull/35244)
|
||||
* Fix - Override filter_meta_data method, since it should be a no-op anyway. [#35192](https://github.com/woocommerce/woocommerce/pull/35192)
|
||||
* Fix - Remove the direct dependency on `$_POST` when validating checkout data. [#35329](https://github.com/woocommerce/woocommerce/pull/35329)
|
||||
* Fix - Revert change that auto collapses the product short description field. [#35213](https://github.com/woocommerce/woocommerce/pull/35213)
|
||||
* Fix - Skip flaky settings API test [#35338](https://github.com/woocommerce/woocommerce/pull/35338)
|
||||
* Fix - Update Playwright from 1.26.1 to 1.27.1 [#35106](https://github.com/woocommerce/woocommerce/pull/35106)
|
||||
* Fix - When the minimum and maximum quantity are identical, render the quantity input and set it to disabled. [#34282](https://github.com/woocommerce/woocommerce/pull/34282)
|
||||
* Add - Add "Empty Trash" functionality to HPOS list table. [#35489](https://github.com/woocommerce/woocommerce/pull/35489)
|
||||
* Add - Add add attribute modal to the attribute field in the new product management MVP [#34999](https://github.com/woocommerce/woocommerce/pull/34999)
|
||||
* Add - Add add new option for the category dropdown within the product MVP [#35132](https://github.com/woocommerce/woocommerce/pull/35132)
|
||||
* Add - Add contextual product more menu [#35447](https://github.com/woocommerce/woocommerce/pull/35447)
|
||||
* Add - Added a guided tour for WooCommerce Extensions page [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Add - Added npm script for Playwright API Core Tests [#35283](https://github.com/woocommerce/woocommerce/pull/35283)
|
||||
* Add - Added states for Senegal. [#35199](https://github.com/woocommerce/woocommerce/pull/35199)
|
||||
* Add - Added the "Tour the WooCommerce Marketplace" task to onboarding tasks list [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Add - Added Ukrainian subdivisions. [#35493](https://github.com/woocommerce/woocommerce/pull/35493)
|
||||
* Add - Adding attribute edit modal for new product screen. [#35269](https://github.com/woocommerce/woocommerce/pull/35269)
|
||||
* Add - Add manual stock management section to product management experience [#35047](https://github.com/woocommerce/woocommerce/pull/35047)
|
||||
* Add - Add new Category dropdown field to the new Product Management screen. [#34400](https://github.com/woocommerce/woocommerce/pull/34400)
|
||||
* Add - add new track events for in-app marketplace tour [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Add - Add option and modal to create new attribute terms within MVP attribute modal. [#35131](https://github.com/woocommerce/woocommerce/pull/35131)
|
||||
* Add - Add placeholder to description field [#35286](https://github.com/woocommerce/woocommerce/pull/35286)
|
||||
* Add - Add playwright api-core-tests for data crud operations [#35347](https://github.com/woocommerce/woocommerce/pull/35347)
|
||||
* Add - Add playwright api-core-tests for payment gateways crud operations [#35279](https://github.com/woocommerce/woocommerce/pull/35279)
|
||||
* Add - Add playwright api-core-tests for product reviews crud operations [#35163](https://github.com/woocommerce/woocommerce/pull/35163)
|
||||
* Add - Add playwright api-core-tests for product variations crud operations [#35355](https://github.com/woocommerce/woocommerce/pull/35355)
|
||||
* Add - Add playwright api-core-tests for reports crud operations [#35388](https://github.com/woocommerce/woocommerce/pull/35388)
|
||||
* Add - Add playwright api-core-tests for settingss crud operations [#35253](https://github.com/woocommerce/woocommerce/pull/35253)
|
||||
* Add - Add playwright api-core-tests for system status crud operations [#35254](https://github.com/woocommerce/woocommerce/pull/35254)
|
||||
* Add - Add playwright api-core-tests for webhooks crud operations [#35292](https://github.com/woocommerce/woocommerce/pull/35292)
|
||||
* Add - Add Product description title in old editor for clarification. [#35154](https://github.com/woocommerce/woocommerce/pull/35154)
|
||||
* Add - Add product inventory advanced section [#35164](https://github.com/woocommerce/woocommerce/pull/35164)
|
||||
* Add - Add product management description to new product management experience [#34961](https://github.com/woocommerce/woocommerce/pull/34961)
|
||||
* Add - Add product state badge to product form header [#35460](https://github.com/woocommerce/woocommerce/pull/35460)
|
||||
* Add - Add product title to header when available [#35431](https://github.com/woocommerce/woocommerce/pull/35431)
|
||||
* Add - Add scheduled sale support to new product edit page. [#34538](https://github.com/woocommerce/woocommerce/pull/34538)
|
||||
* Add - Adds new Inbox Note to provide more information about WooCommerce Payments to users who dismiss the WCPay promo but say that they want more information in the exit survey. [#35581](https://github.com/woocommerce/woocommerce/pull/35581)
|
||||
* Add - Add summary to new product page experience [#35201](https://github.com/woocommerce/woocommerce/pull/35201)
|
||||
* Add - Include order datastore information in status report. [#35487](https://github.com/woocommerce/woocommerce/pull/35487)
|
||||
* Add - Make it possible to add custom bulk action handling to the admin order list screen (when HPOS is enabled). [#35442](https://github.com/woocommerce/woocommerce/pull/35442)
|
||||
* Add - Set In-App Marketplace Tour as completed on tour close [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Add - When custom order tables are not authoritative, admin UI requests will be redirected to the matching legacy order screen as appropriate. [#35463](https://github.com/woocommerce/woocommerce/pull/35463)
|
||||
* Update - Woo Blocks 8.9.2 [#35805](https://github.com/woocommerce/woocommerce/pull/35805)
|
||||
* Update - Comment: Update WooCommerce Blocks to 8.7.2 [#35101](https://github.com/woocommerce/woocommerce/pull/35101)
|
||||
* Update - Comment: Update WooCommerce Blocks to 8.7.3 [#35219](https://github.com/woocommerce/woocommerce/pull/35219)
|
||||
* Update - Comment: Update WooCommerce Blocks to 8.9.1 [#35564](https://github.com/woocommerce/woocommerce/pull/35564)
|
||||
* Update - CustomOrdersTableController::custom_orders_table_usage_is_enabled returns now false if the HPOS feature is disabled [#35597](https://github.com/woocommerce/woocommerce/pull/35597)
|
||||
* Update - Disable inventory stock toggle when product stock management is disabled [#35059](https://github.com/woocommerce/woocommerce/pull/35059)
|
||||
* Update - Improve the loading time of WooCommerce setup widget for large databases [#35334](https://github.com/woocommerce/woocommerce/pull/35334)
|
||||
* Update - Permit showing a guided tour for WooCommerce Extensions page on desktops only [#35278](https://github.com/woocommerce/woocommerce/pull/35278)
|
||||
* Update - Remove adding and managing products note [#35319](https://github.com/woocommerce/woocommerce/pull/35319)
|
||||
* Update - Remove first downloadable product note [#35318](https://github.com/woocommerce/woocommerce/pull/35318)
|
||||
* Update - Remove InsightFirstProductAndPayment note [#35309](https://github.com/woocommerce/woocommerce/pull/35309)
|
||||
* Update - Remove insight on first sale note [#35341](https://github.com/woocommerce/woocommerce/pull/35341)
|
||||
* Update - Remove manage store activity note [#35320](https://github.com/woocommerce/woocommerce/pull/35320)
|
||||
* Update - Remove Popover.Slot usage and make use of exported SelectControlMenuSlot. [#35353](https://github.com/woocommerce/woocommerce/pull/35353)
|
||||
* Update - Remove update store details note [#35322](https://github.com/woocommerce/woocommerce/pull/35322)
|
||||
* Update - Update Array checks in playwright api-core-tests as some of the existing tests would produce false positives [#35462](https://github.com/woocommerce/woocommerce/pull/35462)
|
||||
* Update - Update playwright api-core-tests for shipping crud operations [#35332](https://github.com/woocommerce/woocommerce/pull/35332)
|
||||
* Update - Update playwright api-core-tests to execute for both base test environment and base JN environment with WooCommerce [#35522](https://github.com/woocommerce/woocommerce/pull/35522)
|
||||
* Update - Update products task list UI [#35611](https://github.com/woocommerce/woocommerce/pull/35611)
|
||||
* Update - Update ShippingLabelBanner add_meta_box action to only trigger on shop_order pages and remove deprecated function call. [#35212](https://github.com/woocommerce/woocommerce/pull/35212)
|
||||
* Update - Update WooCommerce Blocks to 8.9.0 [#35521](https://github.com/woocommerce/woocommerce/pull/35521)
|
||||
* Dev - Add variation price shortcut [#34948](https://github.com/woocommerce/woocommerce/pull/34948)
|
||||
* Dev - Cleanup and deprecate unused Task properties and methods [#35450](https://github.com/woocommerce/woocommerce/pull/35450)
|
||||
* Dev - Enable Playwright tests on Daily Smoke Test workflow and upload its Allure reports to S3 bucket. [#35114](https://github.com/woocommerce/woocommerce/pull/35114)
|
||||
* Dev - Move product action buttons to header menu [#35214](https://github.com/woocommerce/woocommerce/pull/35214)
|
||||
* Dev - Revert the changes introduced in PR #35282 [#35337](https://github.com/woocommerce/woocommerce/pull/35337)
|
||||
* Dev - Show a dismissible snackbar if the server responds with an error [#35160](https://github.com/woocommerce/woocommerce/pull/35160)
|
||||
* Dev - Update api-core-tests readme for consistency with new command and updates to other commands too. [#35303](https://github.com/woocommerce/woocommerce/pull/35303)
|
||||
* Dev - Updated the COT plugin URL now that this feature can be enabled in a different way. [#34990](https://github.com/woocommerce/woocommerce/pull/34990)
|
||||
* Dev - Update the list of tags for WC plugin on .org [#35573](https://github.com/woocommerce/woocommerce/pull/35573)
|
||||
* Dev - Update unit test install script for db sockets. [#35152](https://github.com/woocommerce/woocommerce/pull/35152)
|
||||
* Dev - Use plugins/woocommerce/tests/e2e-pw folder for saving test outputs [#35206](https://github.com/woocommerce/woocommerce/pull/35206)
|
||||
* Dev - Uses the globa-setup.js to setup permalinks structure [#35282](https://github.com/woocommerce/woocommerce/pull/35282)
|
||||
* Tweak - Move HPOS hook woocommerce_before_delete_order before deleting order. [#35517](https://github.com/woocommerce/woocommerce/pull/35517)
|
||||
* Tweak - Adds new filter `woocommerce_get_customer_payment_tokens_limit` to set limit on number of payment methods fetched within the My Account page. [#29850](https://github.com/woocommerce/woocommerce/pull/29850)
|
||||
* Tweak - Add source parameter for calls to the subscriptions endpoint on WooCommerce.com [#35051](https://github.com/woocommerce/woocommerce/pull/35051)
|
||||
* Tweak - Fix @version header in form-login.php [#35479](https://github.com/woocommerce/woocommerce/pull/35479)
|
||||
* Tweak - Move HPOS hook woocommerce_before_delete_order before deleting order. [#35517](https://github.com/woocommerce/woocommerce/pull/35517)
|
||||
* Tweak - typo fix [#35111](https://github.com/woocommerce/woocommerce/pull/35111)
|
||||
* Tweak - Unwrap product page input props and pass via getInputProps [#35034](https://github.com/woocommerce/woocommerce/pull/35034)
|
||||
* Tweak - Updates the currency symbol used for the Azerbaijani manat. [#30605](https://github.com/woocommerce/woocommerce/pull/30605)
|
||||
* Tweak - Use new Tooltip component instead of EnrichedLabel [#35024](https://github.com/woocommerce/woocommerce/pull/35024)
|
||||
* Enhancement - Change the product info section title to Product Details [#35255](https://github.com/woocommerce/woocommerce/pull/35255)
|
||||
* Enhancement - Fix the display of letter descenders in the shipping class dropdown menu [#35258](https://github.com/woocommerce/woocommerce/pull/35258)
|
||||
* Enhancement - Improve the communication around required and optional [#35266](https://github.com/woocommerce/woocommerce/pull/35266)
|
||||
* Enhancement - Increase the spacing between the shipping box illustration and the dimensions fields [#35259](https://github.com/woocommerce/woocommerce/pull/35259)
|
||||
* Enhancement - Optimize query usage in the Onboarding tasks [#35065](https://github.com/woocommerce/woocommerce/pull/35065)
|
||||
* Enhancement - Remove some placeholder values [#35267](https://github.com/woocommerce/woocommerce/pull/35267)
|
||||
* Enhancement - Replace the trash can icon in the attribute list [#35133](https://github.com/woocommerce/woocommerce/pull/35133)
|
||||
* Enhancement - Select the current new added shipping class [#35123](https://github.com/woocommerce/woocommerce/pull/35123)
|
||||
* Enhancement - Tweaks the PR template for GitHub pull requests [#34597](https://github.com/woocommerce/woocommerce/pull/34597)
|
||||
|
||||
|
||||
= 7.1.1 2022-12-07 =
|
||||
|
||||
**WooCommerce**
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adding isHidden option for primary button in TourKit component.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add className prop to Sortable
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: enhancement
|
||||
|
||||
Add noDataLabel property into table.js component to allow No Data label customization.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Make Table component accept className prop.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add className prop to ListItem.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Add aria-label for simple select dropdown
|
|
@ -12,19 +12,21 @@ import { SortableHandle } from '../sortable';
|
|||
|
||||
export type ListItemProps = {
|
||||
children: JSX.Element | JSX.Element[] | string;
|
||||
className?: string;
|
||||
onDragStart?: DragEventHandler< HTMLDivElement >;
|
||||
onDragEnd?: DragEventHandler< HTMLDivElement >;
|
||||
};
|
||||
|
||||
export const ListItem = ( {
|
||||
children,
|
||||
className,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
}: ListItemProps ) => {
|
||||
const isDraggable = onDragEnd && onDragStart;
|
||||
|
||||
return (
|
||||
<div className={ classnames( 'woocommerce-list-item' ) }>
|
||||
<div className={ classnames( 'woocommerce-list-item', className ) }>
|
||||
{ isDraggable && <SortableHandle /> }
|
||||
{ children }
|
||||
</div>
|
||||
|
|
|
@ -154,6 +154,7 @@ class Control extends Component {
|
|||
: null
|
||||
}
|
||||
disabled={ disabled }
|
||||
aria-label={ this.props.ariaLabel ?? this.props.label }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -241,6 +241,7 @@ Name | Type | Default | Description
|
|||
`rows` | Array | `null` | (required) An array of arrays of display/value object pairs
|
||||
`rowHeader` | One of type: number, bool | `0` | Which column should be the row header, defaults to the first item (`0`) (but could be set to `1`, if the first col is checkboxes, for example). Set to false to disable row headers
|
||||
`rowKey` | Function(row, index): string | `null` | Function used to get the row key.
|
||||
`emptyMessage` | String | `undefined` | Customize the message to show when there are no rows in the table.
|
||||
|
||||
### `headers` structure
|
||||
|
||||
|
|
|
@ -153,6 +153,7 @@ class TableCard extends Component {
|
|||
title,
|
||||
totalRows,
|
||||
rowKey,
|
||||
emptyMessage,
|
||||
} = this.props;
|
||||
const { showCols } = this.state;
|
||||
const allHeaders = this.props.headers;
|
||||
|
@ -237,6 +238,7 @@ class TableCard extends Component {
|
|||
query={ query }
|
||||
onSort={ onSort || onQueryChange( 'sort' ) }
|
||||
rowKey={ rowKey }
|
||||
emptyMessage={ emptyMessage }
|
||||
/>
|
||||
) }
|
||||
</CardBody>
|
||||
|
@ -361,6 +363,10 @@ TableCard.propTypes = {
|
|||
* This uses the index if not defined.
|
||||
*/
|
||||
rowKey: PropTypes.func,
|
||||
/**
|
||||
* Customize the message to show when there are no rows in the table.
|
||||
*/
|
||||
emptyMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
TableCard.defaultProps = {
|
||||
|
@ -372,6 +378,7 @@ TableCard.defaultProps = {
|
|||
rowHeader: 0,
|
||||
rows: [],
|
||||
showMenu: true,
|
||||
emptyMessage: undefined,
|
||||
};
|
||||
|
||||
export default TableCard;
|
||||
|
|
|
@ -25,7 +25,7 @@ class TablePlaceholder extends Component {
|
|||
return (
|
||||
<Table
|
||||
ariaHidden={ true }
|
||||
classNames="is-loading"
|
||||
className="is-loading"
|
||||
rows={ rows }
|
||||
{ ...tableProps }
|
||||
/>
|
||||
|
|
|
@ -20,6 +20,18 @@ export const Basic = () => (
|
|||
</Card>
|
||||
);
|
||||
|
||||
export const NoDataCustomMessage = () => (
|
||||
<Card size={ null }>
|
||||
<Table
|
||||
caption="Revenue last week"
|
||||
rows={ [] }
|
||||
headers={ headers }
|
||||
rowKey={ ( row ) => row[ 0 ].value }
|
||||
emptyMessage="Custom empty message"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Admin/components/Table',
|
||||
component: Table,
|
||||
|
|
|
@ -14,6 +14,7 @@ import { find, get, noop } from 'lodash';
|
|||
import PropTypes from 'prop-types';
|
||||
import { withInstanceId } from '@wordpress/compose';
|
||||
import { Icon, chevronUp, chevronDown } from '@wordpress/icons';
|
||||
import deprecated from '@wordpress/deprecated';
|
||||
|
||||
const ASC = 'asc';
|
||||
const DESC = 'desc';
|
||||
|
@ -140,18 +141,35 @@ class Table extends Component {
|
|||
const {
|
||||
ariaHidden,
|
||||
caption,
|
||||
className,
|
||||
classNames,
|
||||
headers,
|
||||
instanceId,
|
||||
query,
|
||||
rowHeader,
|
||||
rows,
|
||||
emptyMessage,
|
||||
} = this.props;
|
||||
const { isScrollableRight, isScrollableLeft, tabIndex } = this.state;
|
||||
const classes = classnames( 'woocommerce-table__table', classNames, {
|
||||
'is-scrollable-right': isScrollableRight,
|
||||
'is-scrollable-left': isScrollableLeft,
|
||||
} );
|
||||
|
||||
if ( classNames ) {
|
||||
deprecated( `Table component's classNames prop`, {
|
||||
since: '11.1.0',
|
||||
version: '12.0.0',
|
||||
alternative: 'className',
|
||||
plugin: '@woocommerce/components',
|
||||
} );
|
||||
}
|
||||
|
||||
const classes = classnames(
|
||||
'woocommerce-table__table',
|
||||
classNames,
|
||||
className,
|
||||
{
|
||||
'is-scrollable-right': isScrollableRight,
|
||||
'is-scrollable-left': isScrollableLeft,
|
||||
}
|
||||
);
|
||||
const sortedBy =
|
||||
query.orderby ||
|
||||
get( find( headers, { defaultSort: true } ), 'key', false );
|
||||
|
@ -344,10 +362,11 @@ class Table extends Component {
|
|||
className="woocommerce-table__empty-item"
|
||||
colSpan={ headers.length }
|
||||
>
|
||||
{ __(
|
||||
'No data to display',
|
||||
'woocommerce'
|
||||
) }
|
||||
{ emptyMessage ??
|
||||
__(
|
||||
'No data to display',
|
||||
'woocommerce'
|
||||
) }
|
||||
</td>
|
||||
</tr>
|
||||
) }
|
||||
|
@ -454,6 +473,10 @@ Table.propTypes = {
|
|||
* Defaults to index.
|
||||
*/
|
||||
rowKey: PropTypes.func,
|
||||
/**
|
||||
* Customize the message to show when there are no rows in the table.
|
||||
*/
|
||||
emptyMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
Table.defaultProps = {
|
||||
|
@ -462,6 +485,7 @@ Table.defaultProps = {
|
|||
onSort: noop,
|
||||
query: {},
|
||||
rowHeader: 0,
|
||||
emptyMessage: undefined,
|
||||
};
|
||||
|
||||
export default withInstanceId( Table );
|
||||
|
|
|
@ -10,6 +10,7 @@ import { createElement } from '@wordpress/element';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import TableCard from '../index';
|
||||
import Table from '../table';
|
||||
import mockHeaders from './data/table-mock-headers';
|
||||
import mockData from './data/table-mock-data';
|
||||
import mockSummary from './data/table-mock-summary';
|
||||
|
@ -171,4 +172,69 @@ describe( 'TableCard', () => {
|
|||
'is-left-aligned'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should render the default "No data to display" when there are no data and emptyMessage is unset', () => {
|
||||
render(
|
||||
<TableCard
|
||||
title="My table"
|
||||
headers={ mockHeaders }
|
||||
isLoading={ false }
|
||||
rows={ [] }
|
||||
rowsPerPage={ 5 }
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByText( 'No data to display' )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should render the custom label set in emptyMessage when there are no data.', () => {
|
||||
const emptyMessage = 'My no data label';
|
||||
|
||||
render(
|
||||
<TableCard
|
||||
title="My table"
|
||||
headers={ mockHeaders }
|
||||
isLoading={ false }
|
||||
rows={ [] }
|
||||
rowsPerPage={ 5 }
|
||||
emptyMessage={ emptyMessage }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( screen.queryByText( emptyMessage ) ).toBeInTheDocument();
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'Table', () => {
|
||||
it( 'should accept className prop and renders it in the HTML output', () => {
|
||||
render(
|
||||
<Table
|
||||
className="class-111"
|
||||
caption="Table with className"
|
||||
headers={ mockHeaders }
|
||||
rows={ mockData }
|
||||
/>
|
||||
);
|
||||
|
||||
const el = screen.getByLabelText( 'Table with className' );
|
||||
|
||||
expect( el ).toHaveClass( 'class-111' );
|
||||
} );
|
||||
|
||||
it( 'should still work with classNames prop and renders it in the HTML output, for backward compatibility reason', () => {
|
||||
render(
|
||||
<Table
|
||||
classNames="class-222"
|
||||
caption="Table with classNames"
|
||||
headers={ mockHeaders }
|
||||
rows={ mockData }
|
||||
/>
|
||||
);
|
||||
|
||||
const el = screen.getByLabelText( 'Table with classNames' );
|
||||
|
||||
expect( el ).toHaveClass( 'class-222' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -25,7 +25,7 @@ const StepNavigation: React.FunctionComponent< Props > = ( {
|
|||
const isFirstStep = currentStepIndex === 0;
|
||||
const isLastStep = currentStepIndex === steps.length - 1;
|
||||
|
||||
const { primaryButton = { text: '', isDisabled: false } } =
|
||||
const { primaryButton = { text: '', isDisabled: false, isHidden: false } } =
|
||||
steps[ currentStepIndex ].meta;
|
||||
|
||||
const NextButton = (
|
||||
|
@ -80,6 +80,10 @@ const StepNavigation: React.FunctionComponent< Props > = ( {
|
|||
);
|
||||
};
|
||||
|
||||
if ( primaryButton.isHidden ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-tour-kit-step-navigation">
|
||||
<div className="woocommerce-tour-kit-step-navigation__step">
|
||||
|
|
|
@ -22,6 +22,7 @@ export interface WooStep extends Step {
|
|||
text?: string;
|
||||
/** Disable the button or not. Default to False */
|
||||
isDisabled?: boolean;
|
||||
isHidden?: boolean;
|
||||
};
|
||||
};
|
||||
/** Auto apply the focus state for the element. Default to null */
|
||||
|
|
|
@ -77,7 +77,6 @@ if ( ! class_exists( '{{slugSnakeCase}}' ) ) :
|
|||
|
||||
/**
|
||||
* Cloning is forbidden.
|
||||
*
|
||||
*/
|
||||
public function __clone() {
|
||||
wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', '{{slugSnakeCase}}' ), $this->version );
|
||||
|
@ -85,7 +84,6 @@ if ( ! class_exists( '{{slugSnakeCase}}' ) ) :
|
|||
|
||||
/**
|
||||
* Unserializing instances of this class is forbidden.
|
||||
*
|
||||
*/
|
||||
public function __wakeup() {
|
||||
wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', '{{slugSnakeCase}}' ), $this->version );
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
vendor
|
||||
node_modules
|
||||
.turbo
|
|
@ -0,0 +1,13 @@
|
|||
# Changelog
|
||||
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.1](https://www.npmjs.com/package/@woocommerce/create-woo-extension/v/1.0.1) - 2022-12-20
|
||||
|
||||
- Patch - Fix install scripts [#34385]
|
||||
|
||||
## [1.0.0](https://www.npmjs.com/package/@woocommerce/create-woo-extension/v/1.0.0) - 2022-12-15
|
||||
|
||||
- Patch - Add WC validation [#35947]
|
||||
|
||||
[See legacy changelogs for previous versions](https://github.com/woocommerce/woocommerce/blob/68581955106947918d2b17607a01bdfdf22288a9/packages/js/create-woo-extension/CHANGELOG.md).
|
|
@ -13,7 +13,7 @@ A boilerplate for modern WooCommerce development. This project adds a React page
|
|||
|
||||
```
|
||||
npm install
|
||||
npm build
|
||||
npm run build
|
||||
wp-env start
|
||||
```
|
||||
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Add WC validation
|
|
@ -0,0 +1,3 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: Just some PHP clean up to adhere to coding standards
|
|
@ -10,8 +10,8 @@ module.exports = {
|
|||
],
|
||||
namespace: 'extension',
|
||||
license: 'GPL-3.0+',
|
||||
},
|
||||
customScripts: {
|
||||
postinstall: 'composer install',
|
||||
customScripts: {
|
||||
postinstall: 'composer install',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@woocommerce/create-woo-extension",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "A template to be used with `@wordpress/create-block` to create a WooCommerce extension.",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Extend product variations data store with generate variations actions
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix IdQuery selector for getItem selector in CRUD data stores
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Update IdQuery type on get item selectors
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix up updateItem query in CRUD data store
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add batchUpdate to product variations datastore
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add image to product variation and export types
|
|
@ -18,6 +18,12 @@ createCrudDataStore( {
|
|||
resourceName: 'MyThing',
|
||||
pluralResourceName: 'MyThings',
|
||||
namespace: '/my/rest/namespace',
|
||||
storeConfig: {
|
||||
actions: additionalActions,
|
||||
selectors: additionalSelectors,
|
||||
resolvers: additionalResolvers,
|
||||
controls: additionalControls,
|
||||
}
|
||||
} );
|
||||
```
|
||||
|
||||
|
@ -55,7 +61,7 @@ If the default settings are not adequate for your needs, you can always create y
|
|||
|
||||
```js
|
||||
import { createSelectors } from '../crud/selectors';
|
||||
import { createResolvers } from '../crud/selectors';
|
||||
import { createResolvers } from '../crud/resolvers';
|
||||
import { createActions } from '../crud/actions';
|
||||
import { registerStore, combineReducers } from '@wordpress/data';
|
||||
|
||||
|
|
|
@ -192,10 +192,11 @@ export const createDispatchActions = ( {
|
|||
const item: Item = yield apiFetch( {
|
||||
path: getRestPath(
|
||||
`${ namespace }/${ id }`,
|
||||
cleanQuery( query, namespace ),
|
||||
{},
|
||||
urlParameters
|
||||
),
|
||||
method: 'PUT',
|
||||
data: query,
|
||||
} );
|
||||
|
||||
yield updateItemSuccess( key, item );
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerStore } from '@wordpress/data';
|
||||
import { combineReducers, registerStore, StoreConfig } from '@wordpress/data';
|
||||
import { Reducer } from 'redux';
|
||||
|
||||
/**
|
||||
|
@ -9,7 +9,7 @@ import { Reducer } from 'redux';
|
|||
*/
|
||||
import { createSelectors } from './selectors';
|
||||
import { createDispatchActions } from './actions';
|
||||
import controls from '../controls';
|
||||
import defaultControls from '../controls';
|
||||
import { createResolvers } from './resolvers';
|
||||
import { createReducer, ResourceState } from './reducer';
|
||||
|
||||
|
@ -18,6 +18,7 @@ type CrudDataStore = {
|
|||
resourceName: string;
|
||||
pluralResourceName: string;
|
||||
namespace: string;
|
||||
storeConfig?: Partial< StoreConfig< ResourceState > >;
|
||||
};
|
||||
|
||||
export const createCrudDataStore = ( {
|
||||
|
@ -25,29 +26,44 @@ export const createCrudDataStore = ( {
|
|||
resourceName,
|
||||
namespace,
|
||||
pluralResourceName,
|
||||
storeConfig = {},
|
||||
}: CrudDataStore ) => {
|
||||
const reducer = createReducer();
|
||||
const actions = createDispatchActions( {
|
||||
const crudReducer = createReducer();
|
||||
|
||||
const crudActions = createDispatchActions( {
|
||||
resourceName,
|
||||
namespace,
|
||||
} );
|
||||
const resolvers = createResolvers( {
|
||||
const crudResolvers = createResolvers( {
|
||||
storeName,
|
||||
resourceName,
|
||||
pluralResourceName,
|
||||
namespace,
|
||||
} );
|
||||
const selectors = createSelectors( {
|
||||
const crudSelectors = createSelectors( {
|
||||
resourceName,
|
||||
pluralResourceName,
|
||||
namespace,
|
||||
} );
|
||||
|
||||
const {
|
||||
reducer,
|
||||
actions = {},
|
||||
selectors = {},
|
||||
resolvers = {},
|
||||
controls = {},
|
||||
} = storeConfig;
|
||||
|
||||
registerStore( storeName, {
|
||||
reducer: reducer as Reducer< ResourceState >,
|
||||
actions,
|
||||
selectors,
|
||||
resolvers,
|
||||
controls,
|
||||
reducer: reducer
|
||||
? ( combineReducers( {
|
||||
crudReducer,
|
||||
reducer,
|
||||
} ) as Reducer )
|
||||
: ( crudReducer as Reducer< ResourceState > ),
|
||||
actions: { ...crudActions, ...actions },
|
||||
selectors: { ...crudSelectors, ...selectors },
|
||||
resolvers: { ...crudResolvers, ...resolvers },
|
||||
controls: { ...defaultControls, ...controls },
|
||||
} );
|
||||
};
|
||||
|
|
|
@ -76,7 +76,7 @@ export type CrudSelectors<
|
|||
'': WPDataSelector< typeof getItem >;
|
||||
},
|
||||
ResourceName,
|
||||
IdType,
|
||||
IdQuery,
|
||||
ItemType
|
||||
> &
|
||||
MapSelectors<
|
||||
|
@ -86,7 +86,7 @@ export type CrudSelectors<
|
|||
UpdateError: WPDataSelector< typeof getItemUpdateError >;
|
||||
},
|
||||
ResourceName,
|
||||
IdType,
|
||||
IdQuery,
|
||||
unknown
|
||||
> &
|
||||
MapSelectors<
|
||||
|
|
|
@ -77,7 +77,11 @@ export * from './countries/types';
|
|||
export * from './onboarding/types';
|
||||
export * from './plugins/types';
|
||||
export * from './products/types';
|
||||
export { ProductVariation } from './product-variations/types';
|
||||
export type {
|
||||
ProductVariation,
|
||||
ProductVariationAttribute,
|
||||
ProductVariationImage,
|
||||
} from './product-variations/types';
|
||||
export {
|
||||
QueryProductAttribute,
|
||||
ProductAttributeSelectors,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export enum TYPES {
|
||||
GENERATE_VARIATIONS_ERROR = 'GENERATE_VARIATIONS_ERROR',
|
||||
BATCH_UPDATE_VARIATIONS_ERROR = 'BATCH_UPDATE_VARIATIONS_ERROR',
|
||||
}
|
||||
|
||||
export default TYPES;
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getUrlParameters, getRestPath, parseId } from '../crud/utils';
|
||||
import TYPES from './action-types';
|
||||
import { IdQuery, IdType, Item } from '../crud/types';
|
||||
import { WC_PRODUCT_VARIATIONS_NAMESPACE } from './constants';
|
||||
import type { BatchUpdateRequest, BatchUpdateResponse } from './types';
|
||||
|
||||
export function generateProductVariationsError( key: IdType, error: unknown ) {
|
||||
return {
|
||||
type: TYPES.GENERATE_VARIATIONS_ERROR as const,
|
||||
key,
|
||||
error,
|
||||
errorType: 'GENERATE_VARIATIONS',
|
||||
};
|
||||
}
|
||||
|
||||
export const generateProductVariations = function* ( idQuery: IdQuery ) {
|
||||
const urlParameters = getUrlParameters(
|
||||
WC_PRODUCT_VARIATIONS_NAMESPACE,
|
||||
idQuery
|
||||
);
|
||||
|
||||
try {
|
||||
const result: Item = yield apiFetch( {
|
||||
path: getRestPath(
|
||||
`${ WC_PRODUCT_VARIATIONS_NAMESPACE }/generate`,
|
||||
{},
|
||||
urlParameters
|
||||
),
|
||||
method: 'POST',
|
||||
} );
|
||||
|
||||
return result;
|
||||
} catch ( error ) {
|
||||
const { key } = parseId( idQuery, urlParameters );
|
||||
|
||||
yield generateProductVariationsError( key, error );
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export function batchUpdateProductVariationsError(
|
||||
key: IdType,
|
||||
error: unknown
|
||||
) {
|
||||
return {
|
||||
type: TYPES.BATCH_UPDATE_VARIATIONS_ERROR as const,
|
||||
key,
|
||||
error,
|
||||
errorType: 'BATCH_UPDATE_VARIATIONS',
|
||||
};
|
||||
}
|
||||
|
||||
export function* batchUpdateProductVariations(
|
||||
idQuery: IdQuery,
|
||||
data: BatchUpdateRequest
|
||||
) {
|
||||
const urlParameters = getUrlParameters(
|
||||
WC_PRODUCT_VARIATIONS_NAMESPACE,
|
||||
idQuery
|
||||
);
|
||||
|
||||
try {
|
||||
const result: BatchUpdateResponse = yield apiFetch( {
|
||||
path: getRestPath(
|
||||
`${ WC_PRODUCT_VARIATIONS_NAMESPACE }/batch`,
|
||||
{},
|
||||
urlParameters
|
||||
),
|
||||
method: 'POST',
|
||||
data,
|
||||
} );
|
||||
|
||||
return result;
|
||||
} catch ( error ) {
|
||||
const { key } = parseId( idQuery, urlParameters );
|
||||
|
||||
yield batchUpdateProductVariationsError( key, error );
|
||||
throw error;
|
||||
}
|
||||
}
|
|
@ -3,12 +3,16 @@
|
|||
*/
|
||||
import { STORE_NAME, WC_PRODUCT_VARIATIONS_NAMESPACE } from './constants';
|
||||
import { createCrudDataStore } from '../crud';
|
||||
import * as actions from './actions';
|
||||
|
||||
createCrudDataStore( {
|
||||
storeName: STORE_NAME,
|
||||
resourceName: 'ProductVariation',
|
||||
pluralResourceName: 'ProductVariations',
|
||||
namespace: WC_PRODUCT_VARIATIONS_NAMESPACE,
|
||||
storeConfig: {
|
||||
actions,
|
||||
},
|
||||
} );
|
||||
|
||||
export const EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME = STORE_NAME;
|
||||
|
|
|
@ -15,11 +15,53 @@ export type ProductVariationAttribute = {
|
|||
option: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Product variation - Image properties
|
||||
*/
|
||||
export interface ProductVariationImage {
|
||||
/**
|
||||
* Image ID.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* The date the image was created, in the site's timezone.
|
||||
*/
|
||||
readonly date_created: string;
|
||||
/**
|
||||
* The date the image was created, as GMT.
|
||||
*/
|
||||
readonly date_created_gmt: string;
|
||||
/**
|
||||
* The date the image was last modified, in the site's timezone.
|
||||
*/
|
||||
readonly date_modified: string;
|
||||
/**
|
||||
* The date the image was last modified, as GMT.
|
||||
*/
|
||||
readonly date_modified_gmt: string;
|
||||
/**
|
||||
* Image URL.
|
||||
*/
|
||||
src: string;
|
||||
/**
|
||||
* Image name.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Image alternative text.
|
||||
*/
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export type ProductVariation = Omit<
|
||||
Product,
|
||||
'name' | 'slug' | 'attributes'
|
||||
'name' | 'slug' | 'attributes' | 'images'
|
||||
> & {
|
||||
attributes: ProductVariationAttribute[];
|
||||
/**
|
||||
* Variation image data.
|
||||
*/
|
||||
image?: ProductVariationImage;
|
||||
};
|
||||
|
||||
type Query = Omit< ProductQuery, 'name' >;
|
||||
|
@ -43,3 +85,16 @@ export type ProductVariationSelectors = CrudSelectors<
|
|||
>;
|
||||
|
||||
export type ActionDispatchers = DispatchFromMap< ProductVariationActions >;
|
||||
|
||||
export type BatchUpdateRequest = {
|
||||
create?: Partial< Omit< ProductVariation, 'id' > >[];
|
||||
update?: ( Pick< ProductVariation, 'id' > &
|
||||
Partial< Omit< ProductVariation, 'id' > > )[];
|
||||
delete?: ProductVariation[ 'id' ][];
|
||||
};
|
||||
|
||||
export type BatchUpdateResponse = {
|
||||
create?: ProductVariation[];
|
||||
update?: ProductVariation[];
|
||||
delete?: ProductVariation[];
|
||||
};
|
||||
|
|
|
@ -69,6 +69,7 @@ export type Product< Status = ProductStatus, Type = ProductType > = Omit<
|
|||
id: number;
|
||||
low_stock_amount: number;
|
||||
manage_stock: boolean;
|
||||
menu_order: number;
|
||||
name: string;
|
||||
on_sale: boolean;
|
||||
permalink: string;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Cleanup product task experiment
|
|
@ -34,7 +34,6 @@ export const trackView = async ( taskId: string, variant?: string ) => {
|
|||
} );
|
||||
};
|
||||
|
||||
let experimentalVariant: string | undefined;
|
||||
type WooOnboardingTaskProps = {
|
||||
id: string;
|
||||
variant?: string;
|
||||
|
@ -56,37 +55,15 @@ type WooOnboardingTaskSlotProps = Slot.Props & {
|
|||
*/
|
||||
const WooOnboardingTask: React.FC< WooOnboardingTaskProps > & {
|
||||
Slot: React.VFC< WooOnboardingTaskSlotProps >;
|
||||
} = ( { id, variant, ...props } ) => {
|
||||
useEffect( () => {
|
||||
if ( id === 'products' ) {
|
||||
experimentalVariant = variant;
|
||||
}
|
||||
}, [ id, variant ] );
|
||||
|
||||
} = ( { id, ...props } ) => {
|
||||
return <Fill name={ 'woocommerce_onboarding_task_' + id } { ...props } />;
|
||||
};
|
||||
|
||||
// We need this here just in case the experiment assignment takes awhile to load, so that we don't fire trackView with a blank experimentalVariant
|
||||
// Remove all of the code in this file related to experiments and variants when the product task experiment concludes and never speak of the existence of this code to anyone
|
||||
const pollForExperimentalVariant = ( id: string, count: number ) => {
|
||||
if ( count > 20 ) {
|
||||
trackView( id, 'experiment_timed_out' ); // if we can't fetch experiment after 4 seconds, give up
|
||||
} else if ( experimentalVariant ) {
|
||||
trackView( id, experimentalVariant );
|
||||
} else {
|
||||
setTimeout( () => pollForExperimentalVariant( id, count + 1 ), 200 );
|
||||
}
|
||||
};
|
||||
|
||||
WooOnboardingTask.Slot = ( { id, fillProps } ) => {
|
||||
// The Slot is a React component and this hook works as expected.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect( () => {
|
||||
if ( id === 'products' ) {
|
||||
pollForExperimentalVariant( id, 0 );
|
||||
} else {
|
||||
trackView( id );
|
||||
}
|
||||
trackView( id );
|
||||
}, [ id ] );
|
||||
|
||||
return (
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
useMemo,
|
||||
useEffect,
|
||||
} from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { uniqueId, find } from 'lodash';
|
||||
import { Icon, help as helpIcon, external } from '@wordpress/icons';
|
||||
import { H, Section } from '@woocommerce/components';
|
||||
|
@ -43,9 +43,13 @@ import {
|
|||
import { getUnapprovedReviews } from '../homescreen/activity-panel/reviews/utils';
|
||||
import { ABBREVIATED_NOTIFICATION_SLOT_NAME } from './panels/inbox/abbreviated-notifications-panel';
|
||||
import { getAdminSetting } from '~/utils/admin-settings';
|
||||
import { getUrlParams } from '~/utils';
|
||||
import { useActiveSetupTasklist } from '~/tasks';
|
||||
import { LayoutContext } from '~/layout';
|
||||
import { getSegmentsFromPath } from '~/utils/url-helpers';
|
||||
import { FeedbackIcon } from '~/products/images/feedback-icon';
|
||||
import { STORE_KEY as CES_STORE_KEY } from '~/customer-effort-score-tracks/data/constants';
|
||||
import { ProductFeedbackTour } from '~/guided-tours/add-product-feedback-tour';
|
||||
|
||||
const HelpPanel = lazy( () =>
|
||||
import( /* webpackChunkName: "activity-panels-help" */ './panels/help' )
|
||||
|
@ -202,6 +206,9 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
|
|||
),
|
||||
};
|
||||
} );
|
||||
|
||||
const { showCesModal } = useDispatch( CES_STORE_KEY );
|
||||
|
||||
const { currentUserCan } = useUser();
|
||||
|
||||
const togglePanel = ( { name: tabName }, isTabOpen ) => {
|
||||
|
@ -237,13 +244,23 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
|
|||
return query.page === 'wc-admin' && ! query.path;
|
||||
};
|
||||
|
||||
const isProductPage = () => {
|
||||
const isProductScreen = () => {
|
||||
const [ firstPathSegment ] = getSegmentsFromPath( query.path );
|
||||
return (
|
||||
firstPathSegment === 'add-product' || firstPathSegment === 'product'
|
||||
);
|
||||
};
|
||||
|
||||
const isAddProductPage = () => {
|
||||
const urlParams = getUrlParams( window.location.search );
|
||||
|
||||
return (
|
||||
isEmbedded &&
|
||||
/post-new\.php$/.test( window.location.pathname ) &&
|
||||
urlParams?.post_type === 'product'
|
||||
);
|
||||
};
|
||||
|
||||
const isPerformingSetupTask = () => {
|
||||
return (
|
||||
query.task &&
|
||||
|
@ -264,7 +281,49 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
|
|||
visible:
|
||||
( isEmbedded || ! isHomescreen() ) &&
|
||||
! isPerformingSetupTask() &&
|
||||
! isProductPage(),
|
||||
! isProductScreen(),
|
||||
};
|
||||
|
||||
const feedback = {
|
||||
name: 'feedback',
|
||||
title: __( 'Feedback', 'woocommerce' ),
|
||||
icon: <FeedbackIcon />,
|
||||
onClick: () => {
|
||||
setCurrentTab( 'feedback' );
|
||||
setIsPanelOpen( true );
|
||||
showCesModal(
|
||||
{
|
||||
action: 'product_feedback',
|
||||
title: __(
|
||||
"How's your experience with the product editor?",
|
||||
'woocommerce'
|
||||
),
|
||||
firstQuestion: __(
|
||||
'The product editing screen is easy to use',
|
||||
'woocommerce'
|
||||
),
|
||||
secondQuestion: __(
|
||||
"The product editing screen's functionality meets my needs",
|
||||
'woocommerce'
|
||||
),
|
||||
},
|
||||
{
|
||||
onRecordScore: () => {
|
||||
setCurrentTab( '' );
|
||||
setIsPanelOpen( false );
|
||||
},
|
||||
onCloseModal: () => {
|
||||
setCurrentTab( '' );
|
||||
setIsPanelOpen( false );
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'snackbar',
|
||||
icon: <span>🌟</span>,
|
||||
}
|
||||
);
|
||||
},
|
||||
visible: isAddProductPage(),
|
||||
};
|
||||
|
||||
const setup = {
|
||||
|
@ -284,7 +343,7 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
|
|||
! setupTaskListComplete &&
|
||||
! setupTaskListHidden &&
|
||||
! isHomescreen() &&
|
||||
! isProductPage(),
|
||||
! isProductScreen(),
|
||||
};
|
||||
|
||||
const help = {
|
||||
|
@ -337,6 +396,7 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
|
|||
|
||||
return [
|
||||
activity,
|
||||
feedback,
|
||||
setup,
|
||||
previewSite,
|
||||
previewStore,
|
||||
|
@ -431,6 +491,9 @@ export const ActivityPanel = ( { isEmbedded, query } ) => {
|
|||
clearPanel={ () => clearPanel() }
|
||||
/>
|
||||
</Section>
|
||||
{ isAddProductPage() && (
|
||||
<ProductFeedbackTour currentTab={ currentTab } />
|
||||
) }
|
||||
{ showHelpHighlightTooltip ? (
|
||||
<HighlightTooltip
|
||||
delay={ 1000 }
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { dispatch, resolveSelect } from '@wordpress/data';
|
||||
import { getQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -42,7 +43,7 @@ export const getExitPageData = () => {
|
|||
* @param {string} pageId of page exited early.
|
||||
*/
|
||||
export const addExitPage = ( pageId: string ) => {
|
||||
if ( ! window.localStorage ) {
|
||||
if ( ! ( window.localStorage && allowTracking ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -93,8 +94,8 @@ export const addCustomerEffortScoreExitPageListener = (
|
|||
pageId: string,
|
||||
hasUnsavedChanges: () => boolean
|
||||
) => {
|
||||
eventListeners[ pageId ] = ( event ) => {
|
||||
if ( hasUnsavedChanges() && allowTracking ) {
|
||||
eventListeners[ pageId ] = () => {
|
||||
if ( hasUnsavedChanges() ) {
|
||||
addExitPage( pageId );
|
||||
}
|
||||
};
|
||||
|
@ -205,19 +206,88 @@ function getExitPageCESCopy( pageId: string ): {
|
|||
'woocommerce'
|
||||
),
|
||||
};
|
||||
case 'shop_order_update':
|
||||
return {
|
||||
action: pageId,
|
||||
icon: '📦',
|
||||
noticeLabel: __(
|
||||
'How easy or difficult was it to update this order?',
|
||||
'woocommerce'
|
||||
),
|
||||
title: __(
|
||||
"How's your experience with orders?",
|
||||
'woocommerce'
|
||||
),
|
||||
description: __(
|
||||
'We noticed you started editing an order, then left. How was it? Your feedback will help create a better experience for thousands of merchants like you.',
|
||||
'woocommerce'
|
||||
),
|
||||
firstQuestion: __(
|
||||
'The order editing screen is easy to use',
|
||||
'woocommerce'
|
||||
),
|
||||
secondQuestion: __(
|
||||
"The order details screen's functionality meets my needs",
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
case 'import_products':
|
||||
return {
|
||||
action: pageId,
|
||||
icon: '🔄',
|
||||
noticeLabel: __(
|
||||
'How is your experience with importing products?',
|
||||
'woocommerce'
|
||||
),
|
||||
title: __(
|
||||
`How's your experience with importing products?`,
|
||||
'woocommerce'
|
||||
),
|
||||
description: __(
|
||||
'We noticed you started importing products, then left. How was it? Your feedback will help create a better experience for thousands of merchants like you.',
|
||||
'woocommerce'
|
||||
),
|
||||
firstQuestion: __(
|
||||
'The product import screen is easy to use',
|
||||
'woocommerce'
|
||||
),
|
||||
secondQuestion: __(
|
||||
"The product import screen's functionality meets my needs",
|
||||
'woocommerce'
|
||||
),
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores trigger conditions for exit page actions.
|
||||
*
|
||||
* @param {string} pageId page id.
|
||||
*/
|
||||
function getShouldExitPageFire( pageId: string ) {
|
||||
const conditionPageMap: Record< string, () => boolean > = {
|
||||
import_products: () =>
|
||||
( getQuery() as { page: string } ).page !== 'product_importer',
|
||||
};
|
||||
|
||||
return conditionPageMap[ pageId ] ? conditionPageMap[ pageId ]() : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the exit page list and triggers a CES survey for the first item in the list.
|
||||
*/
|
||||
export function triggerExitPageCesSurvey() {
|
||||
const exitPageItems: string[] = getExitPageData();
|
||||
if ( exitPageItems && exitPageItems.length > 0 ) {
|
||||
if ( exitPageItems?.length ) {
|
||||
if ( ! getShouldExitPageFire( exitPageItems[ 0 ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const copy = getExitPageCESCopy( exitPageItems[ 0 ] );
|
||||
if ( copy && copy.title.length > 0 ) {
|
||||
|
||||
if ( copy?.title?.length ) {
|
||||
dispatch( 'wc/customer-effort-score' ).addCesSurvey( {
|
||||
...copy,
|
||||
pageNow: window.pagenow,
|
||||
|
|
|
@ -74,15 +74,18 @@ export const CustomerEffortScoreModalContainer: React.FC = () => {
|
|||
|
||||
return (
|
||||
<CustomerFeedbackModal
|
||||
title={ visibleCESModalData.label }
|
||||
description={ visibleCESModalData.description }
|
||||
title={ visibleCESModalData.title }
|
||||
firstQuestion={ visibleCESModalData.firstQuestion }
|
||||
secondQuestion={ visibleCESModalData.secondQuestion }
|
||||
recordScoreCallback={ ( ...args ) => {
|
||||
recordScore( ...args );
|
||||
hideCesModal();
|
||||
visibleCESModalData.props?.onRecordScore?.();
|
||||
} }
|
||||
onCloseModal={ () => {
|
||||
visibleCESModalData.props?.onCloseModal?.();
|
||||
hideCesModal();
|
||||
} }
|
||||
onCloseModal={ () => hideCesModal() }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
|
|||
case TYPES.SHOW_CES_MODAL:
|
||||
const cesModalData = {
|
||||
action: action.surveyProps.action,
|
||||
label: action.surveyProps.label,
|
||||
title: action.surveyProps.title,
|
||||
onSubmitLabel: action.onSubmitLabel,
|
||||
firstQuestion: action.surveyProps.firstQuestion,
|
||||
secondQuestion: action.surveyProps.secondQuestion,
|
||||
|
|
|
@ -67,7 +67,7 @@ export const ProductMVPCESFooter: React.FC = () => {
|
|||
showCesModal(
|
||||
{
|
||||
action: cesAction,
|
||||
label: __(
|
||||
title: __(
|
||||
"How's your experience with the product editor?",
|
||||
'woocommerce'
|
||||
),
|
||||
|
|
|
@ -358,7 +358,10 @@ export function StoreAddress( {
|
|||
required
|
||||
autoComplete="new-password" // disable autocomplete and autofill
|
||||
getSearchExpression={ ( query: string ) => {
|
||||
return new RegExp( '^' + query, 'i' );
|
||||
return new RegExp(
|
||||
'(^' + query + '| — (' + query + '))',
|
||||
'i'
|
||||
);
|
||||
} }
|
||||
options={ countryStateOptions }
|
||||
excludeSelectedOptions={ false }
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { TourKit } from '@woocommerce/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
|
||||
const FEEDBACK_TOUR_OPTION = 'woocommerce_ces_product_feedback_shown';
|
||||
const FEEDBACK_TIMEOUT_MS = 7 * 60 * 1000;
|
||||
|
||||
const useShowProductFeedbackTour = (): undefined | boolean => {
|
||||
const { hasShownTour } = useSelect( ( select ) => {
|
||||
const { getOption } = select( OPTIONS_STORE_NAME );
|
||||
|
||||
return {
|
||||
hasShownTour: getOption( FEEDBACK_TOUR_OPTION ) as
|
||||
| boolean
|
||||
| undefined,
|
||||
};
|
||||
} );
|
||||
|
||||
return hasShownTour;
|
||||
};
|
||||
|
||||
type ProductFeedbackTourProps = {
|
||||
currentTab: string;
|
||||
};
|
||||
|
||||
export const ProductFeedbackTour: React.FC< ProductFeedbackTourProps > = ( {
|
||||
currentTab,
|
||||
} ) => {
|
||||
const hasShownTour = useShowProductFeedbackTour();
|
||||
const [ isTourVisible, setIsTourVisible ] = useState( false );
|
||||
const tourTimeout = useRef< ReturnType< typeof setTimeout > | null >(
|
||||
null
|
||||
);
|
||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||
|
||||
const clearTourTimeout = () => {
|
||||
clearTimeout( tourTimeout.current as ReturnType< typeof setTimeout > );
|
||||
tourTimeout.current = null;
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
if ( hasShownTour !== false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
tourTimeout.current = setTimeout( () => {
|
||||
setIsTourVisible( true );
|
||||
}, FEEDBACK_TIMEOUT_MS );
|
||||
|
||||
return () => clearTourTimeout();
|
||||
}, [ hasShownTour ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( ! isTourVisible ) {
|
||||
return;
|
||||
}
|
||||
updateOptions( {
|
||||
[ FEEDBACK_TOUR_OPTION ]: true,
|
||||
} );
|
||||
}, [ isTourVisible ] );
|
||||
|
||||
if (
|
||||
currentTab === 'feedback' &&
|
||||
( isTourVisible || tourTimeout.current )
|
||||
) {
|
||||
setIsTourVisible( false );
|
||||
clearTourTimeout();
|
||||
}
|
||||
|
||||
if ( ! isTourVisible ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TourKit
|
||||
config={ {
|
||||
steps: [
|
||||
{
|
||||
referenceElements: {
|
||||
desktop: '#activity-panel-tab-feedback',
|
||||
},
|
||||
meta: {
|
||||
name: 'product-feedback-tour-1',
|
||||
heading: __( '🫣 Feeling stuck?', 'woocommerce' ),
|
||||
descriptions: {
|
||||
desktop: __(
|
||||
"You have been working on this product for a few minutes now. Is there something you're struggling with? Share your feedback.",
|
||||
'woocommerce'
|
||||
),
|
||||
},
|
||||
primaryButton: {
|
||||
isHidden: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
placement: 'bottom-start',
|
||||
options: {
|
||||
effects: {
|
||||
liveResize: { mutation: true, resize: true },
|
||||
},
|
||||
},
|
||||
closeHandler: () => {
|
||||
setIsTourVisible( false );
|
||||
},
|
||||
} }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -29,7 +29,6 @@ const EditProductPage = lazy( () =>
|
|||
/* webpackChunkName: "edit-product-page" */ '../products/edit-product-page'
|
||||
)
|
||||
);
|
||||
|
||||
const AddProductPage = lazy( () =>
|
||||
import(
|
||||
/* webpackChunkName: "add-product-page" */ '../products/add-product-page'
|
||||
|
@ -46,7 +45,6 @@ const AnalyticsSettings = lazy( () =>
|
|||
const Dashboard = lazy( () =>
|
||||
import( /* webpackChunkName: "dashboard" */ '../dashboard' )
|
||||
);
|
||||
|
||||
const Homescreen = lazy( () =>
|
||||
import( /* webpackChunkName: "homescreen" */ '../homescreen' )
|
||||
);
|
||||
|
@ -66,7 +64,6 @@ const ProfileWizard = lazy( () =>
|
|||
const SettingsGroup = lazy( () =>
|
||||
import( /* webpackChunkName: "profile-wizard" */ '../settings' )
|
||||
);
|
||||
|
||||
const WCPaymentsWelcomePage = lazy( () =>
|
||||
import(
|
||||
/* webpackChunkName: "wcpay-payment-welcome-page" */ '../payments-welcome'
|
||||
|
@ -202,6 +199,20 @@ export const getPages = () => {
|
|||
wpOpenMenu: 'menu-posts-product',
|
||||
capability: 'manage_woocommerce',
|
||||
} );
|
||||
|
||||
pages.push( {
|
||||
container: EditProductPage,
|
||||
path: '/product/:productId/variation/:variationId',
|
||||
breadcrumbs: [
|
||||
[ '/edit-product', __( 'Product', 'woocommerce' ) ],
|
||||
__( 'Edit Product Variation', 'woocommerce' ),
|
||||
],
|
||||
navArgs: {
|
||||
id: 'woocommerce-edit-product',
|
||||
},
|
||||
wpOpenMenu: 'menu-posts-product',
|
||||
capability: 'edit_products',
|
||||
} );
|
||||
}
|
||||
|
||||
if ( window.wcAdminFeatures.onboarding ) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useEffect } from '@wordpress/element';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { ProductForm } from './product-form';
|
||||
import './product-page.scss';
|
||||
|
||||
const AddProductPage: React.FC = () => {
|
||||
useEffect( () => {
|
||||
|
|
|
@ -5,3 +5,4 @@ export const ONLY_ONE_DECIMAL_SEPARATOR = '[%s](?=%s*[%s])';
|
|||
export const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE =
|
||||
'__ADD_NEW_SHIPPING_CLASS_OPTION__';
|
||||
export const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized';
|
||||
export const PRODUCT_VARIATION_TITLE_LIMIT = 32;
|
||||
|
|
|
@ -2,35 +2,43 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useEffect, useRef } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { Spinner, FormRef } from '@woocommerce/components';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||
PartialProduct,
|
||||
Product,
|
||||
PRODUCTS_STORE_NAME,
|
||||
WCDataSelector,
|
||||
} from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useEffect, useRef } from '@wordpress/element';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { Spinner, FormRef } from '@woocommerce/components';
|
||||
import { useParams } from 'react-router-dom';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ProductForm } from './product-form';
|
||||
import { ProductFormLayout } from './layout/product-form-layout';
|
||||
import { ProductVariationForm } from './product-variation-form';
|
||||
import './product-page.scss';
|
||||
|
||||
const EditProductPage: React.FC = () => {
|
||||
const { productId } = useParams();
|
||||
const { productId, variationId } = useParams();
|
||||
const isProductVariation = !! variationId;
|
||||
const previousProductRef = useRef< PartialProduct >();
|
||||
const formRef = useRef< FormRef< Partial< Product > > >( null );
|
||||
const { product, isLoading, isPendingAction } = useSelect(
|
||||
const { product, isLoading, isPendingAction, productVariation } = useSelect(
|
||||
( select: WCDataSelector ) => {
|
||||
const {
|
||||
getProduct,
|
||||
hasFinishedResolution,
|
||||
hasFinishedResolution: hasProductFinishedResolution,
|
||||
isPending,
|
||||
getPermalinkParts,
|
||||
} = select( PRODUCTS_STORE_NAME );
|
||||
const {
|
||||
getProductVariation,
|
||||
hasFinishedResolution: hasProductVariationFinishedResolution,
|
||||
} = select( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
|
||||
if ( productId ) {
|
||||
const retrievedProduct = getProduct(
|
||||
parseInt( productId, 10 ),
|
||||
|
@ -44,13 +52,26 @@ const EditProductPage: React.FC = () => {
|
|||
permalinkParts && retrievedProduct
|
||||
? retrievedProduct
|
||||
: undefined,
|
||||
productVariation:
|
||||
isProductVariation &&
|
||||
getProductVariation( {
|
||||
id: parseInt( variationId, 10 ),
|
||||
product_id: parseInt( productId, 10 ),
|
||||
} ),
|
||||
isLoading:
|
||||
! hasFinishedResolution( 'getProduct', [
|
||||
! hasProductFinishedResolution( 'getProduct', [
|
||||
parseInt( productId, 10 ),
|
||||
] ) ||
|
||||
! hasFinishedResolution( 'getPermalinkParts', [
|
||||
! hasProductFinishedResolution( 'getPermalinkParts', [
|
||||
parseInt( productId, 10 ),
|
||||
] ),
|
||||
] ) ||
|
||||
! (
|
||||
isProductVariation &&
|
||||
hasProductVariationFinishedResolution(
|
||||
'getProductVariation',
|
||||
[ parseInt( variationId, 10 ) ]
|
||||
)
|
||||
),
|
||||
isPendingAction:
|
||||
isPending( 'createProduct' ) ||
|
||||
isPending(
|
||||
|
@ -109,7 +130,14 @@ const EditProductPage: React.FC = () => {
|
|||
</div>
|
||||
</ProductFormLayout>
|
||||
) }
|
||||
{ product &&
|
||||
{ productVariation && product && (
|
||||
<ProductVariationForm
|
||||
product={ product }
|
||||
productVariation={ productVariation }
|
||||
/>
|
||||
) }
|
||||
{ ! isProductVariation &&
|
||||
product &&
|
||||
( product.status !== 'trash' || wasDeletedUsingAction ) && (
|
||||
<ProductForm formRef={ formRef } product={ product } />
|
||||
) }
|
||||
|
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1,14 @@
|
|||
.woocommerce-attribute-empty-state {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__image {
|
||||
max-width: 150px;
|
||||
margin: $gap-larger 0 $gap-large;
|
||||
}
|
||||
&__add-new {
|
||||
margin: $gap-large 0 $gap-larger;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Button, Card, CardBody } from '@wordpress/components';
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './attribute-empty-state.scss';
|
||||
import AttributeEmptyStateLogo from './attribute-empty-state-logo.svg';
|
||||
|
||||
type AttributeEmptyStateProps = {
|
||||
image?: string;
|
||||
subtitle?: string;
|
||||
addNewLabel?: string;
|
||||
onNewClick?: () => void;
|
||||
};
|
||||
|
||||
export const AttributeEmptyState: React.FC< AttributeEmptyStateProps > = ( {
|
||||
image = AttributeEmptyStateLogo,
|
||||
subtitle = __( 'No attributes yet', 'woocommerce' ),
|
||||
addNewLabel = __( 'Add first attribute', 'woocommerce' ),
|
||||
onNewClick,
|
||||
} ) => {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<div className="woocommerce-attribute-empty-state">
|
||||
<img
|
||||
src={ image }
|
||||
alt="Completed"
|
||||
className="woocommerce-attribute-empty-state__image"
|
||||
/>
|
||||
<Text
|
||||
variant="subtitle.small"
|
||||
weight="600"
|
||||
size="14"
|
||||
lineHeight="20px"
|
||||
className="woocommerce-attribute-empty-state__subtitle"
|
||||
>
|
||||
{ subtitle }
|
||||
</Text>
|
||||
{ typeof onNewClick === 'function' && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="woocommerce-attribute-empty-state__add-new"
|
||||
onClick={ onNewClick }
|
||||
>
|
||||
{ addNewLabel }
|
||||
</Button>
|
||||
) }
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from './attribute-empty-state';
|
||||
export { default as AttributeEmptyStateLogo } from './attribute-empty-state-logo.svg';
|
|
@ -30,6 +30,21 @@ import { HydratedAttributeType } from '../attribute-field';
|
|||
import { getProductAttributeObject } from './utils';
|
||||
|
||||
type AddAttributeModalProps = {
|
||||
title?: string;
|
||||
notice?: string;
|
||||
attributeLabel?: string;
|
||||
valueLabel?: string;
|
||||
attributePlaceholder?: string;
|
||||
termPlaceholder?: string;
|
||||
removeLabel?: string;
|
||||
addAnotherAccessibleLabel?: string;
|
||||
addAnotherLabel?: string;
|
||||
cancelLabel?: string;
|
||||
addAccessibleLabel?: string;
|
||||
addLabel?: string;
|
||||
confirmMessage?: string;
|
||||
confirmCancelLabel?: string;
|
||||
confirmConfirmLabel?: string;
|
||||
onCancel: () => void;
|
||||
onAdd: ( newCategories: HydratedAttributeType[] ) => void;
|
||||
selectedAttributeIds?: number[];
|
||||
|
@ -40,6 +55,27 @@ type AttributeForm = {
|
|||
};
|
||||
|
||||
export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
||||
title = __( 'Add attributes', 'woocommerce' ),
|
||||
notice = __(
|
||||
'By default, attributes are filterable and visible on the product page. You can change these settings for each attribute separately later.',
|
||||
'woocommerce'
|
||||
),
|
||||
attributeLabel = __( 'Attribute', 'woocommerce' ),
|
||||
valueLabel = __( 'Values', 'woocommerce' ),
|
||||
attributePlaceholder = __( 'Search or create attribute', 'woocommerce' ),
|
||||
termPlaceholder = __( 'Search or create value', 'woocommerce' ),
|
||||
removeLabel = __( 'Remove attribute', 'woocommerce' ),
|
||||
addAnotherAccessibleLabel = __( 'Add another attribute', 'woocommerce' ),
|
||||
addAnotherLabel = __( '+ Add another', 'woocommerce' ),
|
||||
cancelLabel = __( 'Cancel', 'woocommerce' ),
|
||||
addAccessibleLabel = __( 'Add attributes', 'woocommerce' ),
|
||||
addLabel = __( 'Add', 'woocommerce' ),
|
||||
confirmMessage = __(
|
||||
'You have some attributes added to the list, are you sure you want to cancel?',
|
||||
'woocommerce'
|
||||
),
|
||||
confirmCancelLabel = __( 'No thanks', 'woocommerce' ),
|
||||
confirmConfirmLabel = __( 'Yes please!', 'woocommerce' ),
|
||||
onCancel,
|
||||
onAdd,
|
||||
selectedAttributeIds = [],
|
||||
|
@ -124,9 +160,6 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
}
|
||||
};
|
||||
|
||||
const attributeLabel = __( 'Attribute', 'woocommerce' );
|
||||
const valueLabel = __( 'Values', 'woocommerce' );
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form< AttributeForm >
|
||||
|
@ -144,7 +177,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
} ) => {
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Add attributes', 'woocommerce' ) }
|
||||
title={ title }
|
||||
onRequestClose={ (
|
||||
event:
|
||||
| React.KeyboardEvent< Element >
|
||||
|
@ -158,12 +191,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
className="woocommerce-add-attribute-modal"
|
||||
>
|
||||
<Notice isDismissible={ false }>
|
||||
<p>
|
||||
{ __(
|
||||
'By default, attributes are filterable and visible on the product page. You can change these settings for each attribute separately later.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
<p>{ notice }</p>
|
||||
</Notice>
|
||||
|
||||
<div className="woocommerce-add-attribute-modal__body">
|
||||
|
@ -183,10 +211,9 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
>
|
||||
<td className="woocommerce-add-attribute-modal__table-attribute-column">
|
||||
<AttributeInputField
|
||||
placeholder={ __(
|
||||
'Search or create attribute',
|
||||
'woocommerce'
|
||||
) }
|
||||
placeholder={
|
||||
attributePlaceholder
|
||||
}
|
||||
value={ attribute }
|
||||
label={
|
||||
attributeLabel
|
||||
|
@ -232,10 +259,9 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
{ attribute === null ||
|
||||
attribute.id !== 0 ? (
|
||||
<AttributeTermInputField
|
||||
placeholder={ __(
|
||||
'Search or create value',
|
||||
'woocommerce'
|
||||
) }
|
||||
placeholder={
|
||||
termPlaceholder
|
||||
}
|
||||
disabled={
|
||||
attribute
|
||||
? ! attribute.id
|
||||
|
@ -268,10 +294,9 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
/>
|
||||
) : (
|
||||
<CustomAttributeTermInputField
|
||||
placeholder={ __(
|
||||
'Search or create value',
|
||||
'woocommerce'
|
||||
) }
|
||||
placeholder={
|
||||
termPlaceholder
|
||||
}
|
||||
disabled={
|
||||
! attribute.name
|
||||
}
|
||||
|
@ -306,10 +331,9 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
.attributes[ 0 ] ===
|
||||
null
|
||||
}
|
||||
label={ __(
|
||||
'Remove attribute',
|
||||
'woocommerce'
|
||||
) }
|
||||
label={
|
||||
removeLabel
|
||||
}
|
||||
onClick={ () =>
|
||||
onRemove(
|
||||
index,
|
||||
|
@ -329,10 +353,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
<Button
|
||||
className="woocommerce-add-attribute-modal__add-attribute"
|
||||
variant="tertiary"
|
||||
label={ __(
|
||||
'Add another attribute',
|
||||
'woocommerce'
|
||||
) }
|
||||
label={ addAnotherAccessibleLabel }
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_add_attributes_modal_add_another_attribute_button_click'
|
||||
|
@ -340,24 +361,20 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
addAnother( values, setValue );
|
||||
} }
|
||||
>
|
||||
+
|
||||
{ __( 'Add another', 'woocommerce' ) }
|
||||
{ addAnotherLabel }
|
||||
</Button>
|
||||
</div>
|
||||
<div className="woocommerce-add-attribute-modal__buttons">
|
||||
<Button
|
||||
isSecondary
|
||||
label={ __( 'Cancel', 'woocommerce' ) }
|
||||
label={ cancelLabel }
|
||||
onClick={ () => onClose( values ) }
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
{ cancelLabel }
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
label={ __(
|
||||
'Add attributes',
|
||||
'woocommerce'
|
||||
) }
|
||||
label={ addAccessibleLabel }
|
||||
disabled={
|
||||
values.attributes.length === 1 &&
|
||||
values.attributes[ 0 ] === null
|
||||
|
@ -366,7 +383,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
onAddingAttributes( values )
|
||||
}
|
||||
>
|
||||
{ __( 'Add', 'woocommerce' ) }
|
||||
{ addLabel }
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -377,15 +394,12 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
<SelectControlMenuSlot />
|
||||
{ showConfirmClose && (
|
||||
<ConfirmDialog
|
||||
cancelButtonText={ __( 'No thanks', 'woocommerce' ) }
|
||||
confirmButtonText={ __( 'Yes please!', 'woocommerce' ) }
|
||||
cancelButtonText={ confirmCancelLabel }
|
||||
confirmButtonText={ confirmConfirmLabel }
|
||||
onCancel={ () => setShowConfirmClose( false ) }
|
||||
onConfirm={ onCancel }
|
||||
>
|
||||
{ __(
|
||||
'You have some attributes added to the list, are you sure you want to cancel?',
|
||||
'woocommerce'
|
||||
) }
|
||||
{ confirmMessage }
|
||||
</ConfirmDialog>
|
||||
) }
|
||||
</>
|
||||
|
|
|
@ -1,63 +1,14 @@
|
|||
.woocommerce-attribute-field {
|
||||
width: 100%;
|
||||
|
||||
&__empty-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
&__empty-logo {
|
||||
max-width: 150px;
|
||||
margin: $gap-larger 0 $gap-large;
|
||||
}
|
||||
&__add-new {
|
||||
margin: $gap-large 0 $gap-larger;
|
||||
}
|
||||
&__attribute-option-chip {
|
||||
padding: $gap-smallest $gap-smaller;
|
||||
gap: 2px;
|
||||
|
||||
background: $gray-100;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__attribute-options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $gap-smallest;
|
||||
}
|
||||
|
||||
&__attribute-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
gap: $gap-smaller;
|
||||
}
|
||||
|
||||
.woocommerce-list-item {
|
||||
min-height: 82px;
|
||||
padding: 0 $gap-large;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-sortable {
|
||||
margin: 0;
|
||||
|
||||
.woocommerce-list-item {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 26% auto 90px;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-sortable__item:not(:first-child) {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.woocommerce-sortable__item:focus-visible:not(:active) + .woocommerce-sortable__item .woocommerce-list-item {
|
||||
.woocommerce-sortable__item:focus-visible:not(:active) + .woocommerce-sortable__item .woocommerce-attribute-list-item {
|
||||
background: none;
|
||||
border-top: 0;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { sprintf, __ } from '@wordpress/i18n';
|
||||
import { Button, Card, CardBody } from '@wordpress/components';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
||||
import {
|
||||
ProductAttribute,
|
||||
|
@ -10,46 +9,51 @@ import {
|
|||
ProductAttributeTerm,
|
||||
} from '@woocommerce/data';
|
||||
import { resolveSelect } from '@wordpress/data';
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
import {
|
||||
Sortable,
|
||||
ListItem,
|
||||
__experimentalSelectControlMenuSlot as SelectControlMenuSlot,
|
||||
Link,
|
||||
} from '@woocommerce/components';
|
||||
import { closeSmall } from '@wordpress/icons';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './attribute-field.scss';
|
||||
import AttributeEmptyStateLogo from './attribute-empty-state-logo.svg';
|
||||
import { AddAttributeModal } from './add-attribute-modal';
|
||||
import { EditAttributeModal } from './edit-attribute-modal';
|
||||
import { reorderSortableProductAttributePositions } from './utils';
|
||||
import { sift } from '../../../utils';
|
||||
import { AttributeEmptyState } from '../attribute-empty-state';
|
||||
import {
|
||||
AddAttributeListItem,
|
||||
AttributeListItem,
|
||||
} from '../attribute-list-item';
|
||||
|
||||
type AttributeFieldProps = {
|
||||
value: ProductAttribute[];
|
||||
onChange: ( value: ProductAttribute[] ) => void;
|
||||
productId?: number;
|
||||
// TODO: should we support an 'any' option to show all attributes?
|
||||
attributeType?: 'regular' | 'for-variations';
|
||||
};
|
||||
|
||||
export type HydratedAttributeType = Omit< ProductAttribute, 'options' > & {
|
||||
options?: string[];
|
||||
terms?: ProductAttributeTerm[];
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||
value,
|
||||
onChange,
|
||||
productId,
|
||||
attributeType = 'regular',
|
||||
} ) => {
|
||||
const [ showAddAttributeModal, setShowAddAttributeModal ] =
|
||||
useState( false );
|
||||
const [ hydrationComplete, setHydrationComplete ] = useState< boolean >(
|
||||
value ? false : true
|
||||
);
|
||||
const [ hydratedAttributes, setHydratedAttributes ] = useState<
|
||||
HydratedAttributeType[]
|
||||
>( [] );
|
||||
|
@ -57,8 +61,13 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
null | string
|
||||
>( null );
|
||||
|
||||
const CANCEL_BUTTON_EVENT_NAME =
|
||||
'product_add_attributes_modal_cancel_button_click';
|
||||
const isOnlyForVariations = attributeType === 'for-variations';
|
||||
|
||||
const newAttributeProps = { variation: isOnlyForVariations };
|
||||
|
||||
const CANCEL_BUTTON_EVENT_NAME = isOnlyForVariations
|
||||
? 'product_add_options_modal_cancel_button_click'
|
||||
: 'product_add_attributes_modal_cancel_button_click';
|
||||
|
||||
const fetchTerms = useCallback(
|
||||
( attributeId: number ) => {
|
||||
|
@ -82,7 +91,12 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
);
|
||||
|
||||
useEffect( () => {
|
||||
if ( ! value || hydrationComplete ) {
|
||||
// I think we'll need to move the hydration out of the individual component
|
||||
// instance. To where, I do not yet know... maybe in the form context
|
||||
// somewhere so that a single hydration source can be shared between multiple
|
||||
// instances? Something like a simple key-value store in the form context
|
||||
// would be handy.
|
||||
if ( ! value || hydratedAttributes.length !== 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -94,19 +108,26 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
).then( ( allResults ) => {
|
||||
setHydratedAttributes( [
|
||||
...globalAttributes.map( ( attr, index ) => {
|
||||
const fetchedTerms = allResults[ index ];
|
||||
|
||||
const newAttr = {
|
||||
...attr,
|
||||
terms: allResults[ index ],
|
||||
options: undefined,
|
||||
// I'm not sure this is quite right for handling unpersisted terms,
|
||||
// but this gets things kinda working for now
|
||||
terms:
|
||||
fetchedTerms.length > 0 ? fetchedTerms : undefined,
|
||||
options:
|
||||
fetchedTerms.length === 0
|
||||
? attr.options
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return newAttr;
|
||||
} ),
|
||||
...customAttributes,
|
||||
] );
|
||||
setHydrationComplete( true );
|
||||
} );
|
||||
}, [ productId, value, hydrationComplete ] );
|
||||
}, [ fetchTerms, hydratedAttributes, value ] );
|
||||
|
||||
const fetchAttributeId = ( attribute: { id: number; name: string } ) =>
|
||||
`${ attribute.id }-${ attribute.name }`;
|
||||
|
@ -121,6 +142,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
? attr.terms.map( ( term ) => term.name )
|
||||
: ( attr.options as string[] ),
|
||||
terms: undefined,
|
||||
visible: attr.visible || false,
|
||||
};
|
||||
} )
|
||||
);
|
||||
|
@ -157,68 +179,70 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
)
|
||||
)
|
||||
.map( ( newAttr, index ) => {
|
||||
newAttr.position = ( value || [] ).length + index;
|
||||
return newAttr;
|
||||
return {
|
||||
...newAttributeProps,
|
||||
...newAttr,
|
||||
position: ( value || [] ).length + index,
|
||||
};
|
||||
} ),
|
||||
] );
|
||||
recordEvent( 'product_add_attributes_modal_add_button_click' );
|
||||
setShowAddAttributeModal( false );
|
||||
};
|
||||
|
||||
if ( ! value || value.length === 0 || hydratedAttributes.length === 0 ) {
|
||||
const filteredAttributes = value
|
||||
? value.filter(
|
||||
( attribute: ProductAttribute ) =>
|
||||
attribute.variation === isOnlyForVariations
|
||||
)
|
||||
: false;
|
||||
|
||||
if (
|
||||
! filteredAttributes ||
|
||||
filteredAttributes.length === 0 ||
|
||||
hydratedAttributes.length === 0
|
||||
) {
|
||||
return (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<div className="woocommerce-attribute-field">
|
||||
<div className="woocommerce-attribute-field__empty-container">
|
||||
<img
|
||||
src={ AttributeEmptyStateLogo }
|
||||
alt="Completed"
|
||||
className="woocommerce-attribute-field__empty-logo"
|
||||
/>
|
||||
<Text
|
||||
variant="subtitle.small"
|
||||
weight="600"
|
||||
size="14"
|
||||
lineHeight="20px"
|
||||
className="woocommerce-attribute-field__empty-subtitle"
|
||||
>
|
||||
{ __( 'No attributes yet', 'woocommerce' ) }
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="woocommerce-attribute-field__add-new"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_add_first_attribute_button_click'
|
||||
);
|
||||
setShowAddAttributeModal( true );
|
||||
} }
|
||||
>
|
||||
{ __( 'Add first attribute', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
{ showAddAttributeModal && (
|
||||
<AddAttributeModal
|
||||
onCancel={ () => {
|
||||
recordEvent( CANCEL_BUTTON_EVENT_NAME );
|
||||
setShowAddAttributeModal( false );
|
||||
} }
|
||||
onAdd={ onAddNewAttributes }
|
||||
selectedAttributeIds={ ( value || [] ).map(
|
||||
( attr ) => attr.id
|
||||
) }
|
||||
/>
|
||||
<>
|
||||
<AttributeEmptyState
|
||||
addNewLabel={
|
||||
isOnlyForVariations
|
||||
? __( 'Add options', 'woocommerce' )
|
||||
: undefined
|
||||
}
|
||||
onNewClick={ () => {
|
||||
recordEvent(
|
||||
'product_add_first_attribute_button_click'
|
||||
);
|
||||
setShowAddAttributeModal( true );
|
||||
} }
|
||||
subtitle={
|
||||
isOnlyForVariations
|
||||
? __( 'No options yet', 'woocommerce' )
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{ showAddAttributeModal && (
|
||||
<AddAttributeModal
|
||||
onCancel={ () => {
|
||||
recordEvent( CANCEL_BUTTON_EVENT_NAME );
|
||||
setShowAddAttributeModal( false );
|
||||
} }
|
||||
onAdd={ onAddNewAttributes }
|
||||
selectedAttributeIds={ ( filteredAttributes || [] ).map(
|
||||
( attr ) => attr.id
|
||||
) }
|
||||
<SelectControlMenuSlot />
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
/>
|
||||
) }
|
||||
<SelectControlMenuSlot />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const sortedAttributes = value.sort( ( a, b ) => a.position - b.position );
|
||||
const attributeKeyValues = value.reduce(
|
||||
const sortedAttributes = filteredAttributes.sort(
|
||||
( a, b ) => a.position - b.position
|
||||
);
|
||||
const attributeKeyValues = filteredAttributes.reduce(
|
||||
(
|
||||
keyValue: Record< number, ProductAttribute >,
|
||||
attribute: ProductAttribute
|
||||
|
@ -229,6 +253,20 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
{} as Record< number, ProductAttribute >
|
||||
);
|
||||
|
||||
const attribute = hydratedAttributes.find(
|
||||
( attr ) => fetchAttributeId( attr ) === editingAttributeId
|
||||
) as HydratedAttributeType;
|
||||
|
||||
const editAttributeCopy = isOnlyForVariations
|
||||
? __(
|
||||
`You can change the option's name in {{link}}Attributes{{/link}}.`,
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
`You can change the attribute's name in {{link}}Attributes{{/link}}.`,
|
||||
'woocommerce'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="woocommerce-attribute-field">
|
||||
<Sortable
|
||||
|
@ -241,66 +279,39 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
);
|
||||
} }
|
||||
>
|
||||
{ sortedAttributes.map( ( attribute ) => (
|
||||
<ListItem key={ fetchAttributeId( attribute ) }>
|
||||
<div>{ attribute.name }</div>
|
||||
<div className="woocommerce-attribute-field__attribute-options">
|
||||
{ attribute.options
|
||||
.slice( 0, 2 )
|
||||
.map( ( option, index ) => (
|
||||
<div
|
||||
className="woocommerce-attribute-field__attribute-option-chip"
|
||||
key={ index }
|
||||
>
|
||||
{ option }
|
||||
</div>
|
||||
) ) }
|
||||
{ attribute.options.length > 2 && (
|
||||
<div className="woocommerce-attribute-field__attribute-option-chip">
|
||||
{ sprintf(
|
||||
__( '+ %i more', 'woocommerce' ),
|
||||
attribute.options.length - 2
|
||||
) }
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
<div className="woocommerce-attribute-field__attribute-actions">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={ () =>
|
||||
setEditingAttributeId(
|
||||
fetchAttributeId( attribute )
|
||||
)
|
||||
}
|
||||
>
|
||||
{ __( 'edit', 'woocommerce' ) }
|
||||
</Button>
|
||||
<Button
|
||||
icon={ closeSmall }
|
||||
label={ __(
|
||||
'Remove attribute',
|
||||
'woocommerce'
|
||||
) }
|
||||
onClick={ () => onRemove( attribute ) }
|
||||
></Button>
|
||||
</div>
|
||||
</ListItem>
|
||||
{ sortedAttributes.map( ( attr ) => (
|
||||
<AttributeListItem
|
||||
attribute={ attr }
|
||||
key={ fetchAttributeId( attr ) }
|
||||
onEditClick={ () =>
|
||||
setEditingAttributeId( fetchAttributeId( attr ) )
|
||||
}
|
||||
onRemoveClick={ () => onRemove( attr ) }
|
||||
/>
|
||||
) ) }
|
||||
</Sortable>
|
||||
<ListItem>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="woocommerce-attribute-field__add-attribute"
|
||||
onClick={ () => {
|
||||
recordEvent( 'product_add_attribute_button' );
|
||||
setShowAddAttributeModal( true );
|
||||
} }
|
||||
>
|
||||
{ __( 'Add attribute', 'woocommerce' ) }
|
||||
</Button>
|
||||
</ListItem>
|
||||
<AddAttributeListItem
|
||||
label={
|
||||
isOnlyForVariations
|
||||
? __( 'Add option', 'woocommerce' )
|
||||
: undefined
|
||||
}
|
||||
onAddClick={ () => {
|
||||
recordEvent(
|
||||
isOnlyForVariations
|
||||
? 'product_add_option_button'
|
||||
: 'product_add_attribute_button'
|
||||
);
|
||||
setShowAddAttributeModal( true );
|
||||
} }
|
||||
/>
|
||||
{ showAddAttributeModal && (
|
||||
<AddAttributeModal
|
||||
title={
|
||||
isOnlyForVariations
|
||||
? __( 'Add options', 'woocommerce' )
|
||||
: undefined
|
||||
}
|
||||
onCancel={ () => {
|
||||
recordEvent( CANCEL_BUTTON_EVENT_NAME );
|
||||
setShowAddAttributeModal( false );
|
||||
|
@ -312,12 +323,37 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
<SelectControlMenuSlot />
|
||||
{ editingAttributeId && (
|
||||
<EditAttributeModal
|
||||
title={
|
||||
/* translators: %s is the attribute name */
|
||||
sprintf(
|
||||
__( 'Edit %s', 'woocommerce' ),
|
||||
attribute.name
|
||||
)
|
||||
}
|
||||
globalAttributeHelperMessage={ interpolateComponents( {
|
||||
mixedString: editAttributeCopy,
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href={ getAdminLink(
|
||||
'edit.php?post_type=product&page=product_attributes'
|
||||
) }
|
||||
target="_blank"
|
||||
type="wp-admin"
|
||||
>
|
||||
<></>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
onCancel={ () => setEditingAttributeId( null ) }
|
||||
onEdit={ ( changedAttribute ) => {
|
||||
const newAttributesSet = [ ...hydratedAttributes ];
|
||||
const changedAttributeIndex: number =
|
||||
newAttributesSet.findIndex(
|
||||
( attr ) => attr.id === changedAttribute.id
|
||||
newAttributesSet.findIndex( ( attr ) =>
|
||||
attr.id !== 0
|
||||
? attr.id === changedAttribute.id
|
||||
: attr.name === changedAttribute.name
|
||||
);
|
||||
|
||||
newAttributesSet.splice(
|
||||
|
@ -329,12 +365,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
updateAttributes( newAttributesSet );
|
||||
setEditingAttributeId( null );
|
||||
} }
|
||||
attribute={
|
||||
hydratedAttributes.find(
|
||||
( attr ) =>
|
||||
fetchAttributeId( attr ) === editingAttributeId
|
||||
) as HydratedAttributeType
|
||||
}
|
||||
attribute={ attribute }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
|
|
|
@ -9,12 +9,7 @@ import {
|
|||
TextControl,
|
||||
} from '@wordpress/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import {
|
||||
__experimentalTooltip as Tooltip,
|
||||
Link,
|
||||
} from '@woocommerce/components';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
import { __experimentalTooltip as Tooltip } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -28,12 +23,42 @@ import { HydratedAttributeType } from './attribute-field';
|
|||
import './edit-attribute-modal.scss';
|
||||
|
||||
type EditAttributeModalProps = {
|
||||
title?: string;
|
||||
nameLabel?: string;
|
||||
globalAttributeHelperMessage?: JSX.Element;
|
||||
customAttributeHelperMessage?: string;
|
||||
termsLabel?: string;
|
||||
termsPlaceholder?: string;
|
||||
visibleLabel?: string;
|
||||
visibleTooltip?: string;
|
||||
cancelAccessibleLabel?: string;
|
||||
cancelLabel?: string;
|
||||
updateAccessibleLabel?: string;
|
||||
updateLabel?: string;
|
||||
onCancel: () => void;
|
||||
onEdit: ( alteredAttribute: HydratedAttributeType ) => void;
|
||||
attribute: HydratedAttributeType;
|
||||
};
|
||||
|
||||
export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
||||
title = __( 'Edit attribute', 'woocommerce' ),
|
||||
nameLabel = __( 'Name', 'woocommerce' ),
|
||||
globalAttributeHelperMessage,
|
||||
customAttributeHelperMessage = __(
|
||||
'Your customers will see this on the product page',
|
||||
'woocommerce'
|
||||
),
|
||||
termsLabel = __( 'Values', 'woocommerce' ),
|
||||
termsPlaceholder = __( 'Search or create value', 'woocommerce' ),
|
||||
visibleLabel = __( 'Visible to customers', 'woocommerce' ),
|
||||
visibleTooltip = __(
|
||||
'Show or hide this attribute on the product page',
|
||||
'woocommerce'
|
||||
),
|
||||
cancelAccessibleLabel = __( 'Cancel', 'woocommerce' ),
|
||||
cancelLabel = __( 'Cancel', 'woocommerce' ),
|
||||
updateAccessibleLabel = __( 'Edit attribute', 'woocommerce' ),
|
||||
updateLabel = __( 'Update', 'woocommerce' ),
|
||||
onCancel,
|
||||
onEdit,
|
||||
attribute,
|
||||
|
@ -46,13 +71,13 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Edit attribute', 'woocommerce' ) }
|
||||
title={ title }
|
||||
onRequestClose={ () => onCancel() }
|
||||
className="woocommerce-edit-attribute-modal"
|
||||
>
|
||||
<div className="woocommerce-edit-attribute-modal__body">
|
||||
<TextControl
|
||||
label={ __( 'Name', 'woocommerce' ) }
|
||||
label={ nameLabel }
|
||||
disabled={ ! isCustomAttribute }
|
||||
value={
|
||||
editableAttribute?.name ? editableAttribute?.name : ''
|
||||
|
@ -66,37 +91,13 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
/>
|
||||
<p className="woocommerce-edit-attribute-modal__helper-text">
|
||||
{ ! isCustomAttribute
|
||||
? interpolateComponents( {
|
||||
mixedString: __(
|
||||
`You can change the attribute's name in {{link}}Attributes{{/link}}.`,
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href={ getAdminLink(
|
||||
'edit.php?post_type=product&page=product_attributes'
|
||||
) }
|
||||
target="_blank"
|
||||
type="wp-admin"
|
||||
>
|
||||
<></>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
} )
|
||||
: __(
|
||||
'Your customers will see this on the product page',
|
||||
'woocommerce'
|
||||
) }
|
||||
? globalAttributeHelperMessage
|
||||
: customAttributeHelperMessage }
|
||||
</p>
|
||||
{ attribute.terms ? (
|
||||
<AttributeTermInputField
|
||||
label={ __( 'Values', 'woocommerce' ) }
|
||||
placeholder={ __(
|
||||
'Search or create value',
|
||||
'woocommerce'
|
||||
) }
|
||||
label={ termsLabel }
|
||||
placeholder={ termsPlaceholder }
|
||||
value={ editableAttribute?.terms }
|
||||
attributeId={ editableAttribute?.id }
|
||||
onChange={ ( val ) => {
|
||||
|
@ -108,11 +109,8 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
/>
|
||||
) : (
|
||||
<CustomAttributeTermInputField
|
||||
label={ __( 'Values', 'woocommerce' ) }
|
||||
placeholder={ __(
|
||||
'Search or create value',
|
||||
'woocommerce'
|
||||
) }
|
||||
label={ termsLabel }
|
||||
placeholder={ termsPlaceholder }
|
||||
disabled={ ! attribute?.name }
|
||||
value={ editableAttribute?.options }
|
||||
onChange={ ( val ) => {
|
||||
|
@ -133,50 +131,27 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
} )
|
||||
}
|
||||
checked={ editableAttribute?.visible }
|
||||
label={ __( 'Visible to customers', 'woocommerce' ) }
|
||||
/>
|
||||
<Tooltip
|
||||
text={ __(
|
||||
'Show or hide this attribute on the product page',
|
||||
'woocommerce'
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
<div className="woocommerce-edit-attribute-modal__option-container">
|
||||
<CheckboxControl
|
||||
onChange={ ( val ) =>
|
||||
setEditableAttribute( {
|
||||
...( editableAttribute as HydratedAttributeType ),
|
||||
variation: val,
|
||||
} )
|
||||
}
|
||||
checked={ editableAttribute?.variation }
|
||||
label={ __( 'Used for filters', 'woocommerce' ) }
|
||||
/>
|
||||
<Tooltip
|
||||
text={ __(
|
||||
`Show or hide this attribute in the filters section on your store's category and shop pages`,
|
||||
'woocommerce'
|
||||
) }
|
||||
label={ visibleLabel }
|
||||
/>
|
||||
<Tooltip text={ visibleTooltip } />
|
||||
</div>
|
||||
</div>
|
||||
<div className="woocommerce-add-attribute-modal__buttons">
|
||||
<Button
|
||||
isSecondary
|
||||
label={ __( 'Cancel', 'woocommerce' ) }
|
||||
label={ cancelAccessibleLabel }
|
||||
onClick={ () => onCancel() }
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
{ cancelLabel }
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary
|
||||
label={ __( 'Edit attribute', 'woocommerce' ) }
|
||||
label={ updateAccessibleLabel }
|
||||
onClick={ () => {
|
||||
onEdit( editableAttribute as HydratedAttributeType );
|
||||
} }
|
||||
>
|
||||
{ __( 'Update', 'woocommerce' ) }
|
||||
{ updateLabel }
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -26,7 +26,7 @@ const attributeList: ProductAttribute[] = [
|
|||
visible: true,
|
||||
variation: true,
|
||||
options: [
|
||||
'Beige',
|
||||
'beige',
|
||||
'black',
|
||||
'Blue',
|
||||
'brown',
|
||||
|
@ -134,23 +134,24 @@ describe( 'AttributeField', () => {
|
|||
await screen.findByText( attributeList[ 0 ].name )
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText( attributeList[ 1 ].name )
|
||||
).toBeInTheDocument();
|
||||
await screen.queryByText( attributeList[ 1 ].name )
|
||||
).not.toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should render the first two terms of each attribute, and show "+ n more" for the rest', async () => {
|
||||
it( 'should render the first two terms of each option, and show "+ n more" for the rest', async () => {
|
||||
act( () => {
|
||||
render(
|
||||
<AttributeField
|
||||
value={ [ ...attributeList ] }
|
||||
onChange={ () => {} }
|
||||
attributeType="for-variations"
|
||||
/>
|
||||
);
|
||||
} );
|
||||
|
||||
expect(
|
||||
await screen.findByText( attributeList[ 0 ].options[ 0 ] )
|
||||
).toBeInTheDocument();
|
||||
await screen.queryByText( attributeList[ 0 ].options[ 0 ] )
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText( attributeList[ 1 ].options[ 0 ] )
|
||||
).toBeInTheDocument();
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { DragEventHandler } from 'react';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { ListItem } from '@woocommerce/components';
|
||||
|
||||
type AddAttributeListItemProps = {
|
||||
label?: string;
|
||||
onAddClick?: () => void;
|
||||
};
|
||||
|
||||
export const AddAttributeListItem: React.FC< AddAttributeListItemProps > = ( {
|
||||
label = __( 'Add attribute', 'woocommerce' ),
|
||||
onAddClick,
|
||||
} ) => {
|
||||
return (
|
||||
<ListItem className="woocommerce-add-attribute-list-item">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="woocommerce-add-attribute-list-item__add-button"
|
||||
onClick={ onAddClick }
|
||||
>
|
||||
{ label }
|
||||
</Button>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
.woocommerce-add-attribute-list-item,
|
||||
.woocommerce-attribute-list-item {
|
||||
min-height: 82px;
|
||||
padding: 0 $gap-large;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-attribute-list-item {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 26% auto 90px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
&__options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $gap-smallest;
|
||||
}
|
||||
|
||||
&__option-chip {
|
||||
padding: $gap-smallest $gap-smaller;
|
||||
gap: 2px;
|
||||
|
||||
background: $gray-100;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
gap: $gap-smaller;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { DragEventHandler } from 'react';
|
||||
import { ListItem } from '@woocommerce/components';
|
||||
import { ProductAttribute } from '@woocommerce/data';
|
||||
import { sprintf, __ } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { closeSmall } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './attribute-list-item.scss';
|
||||
|
||||
type AttributeListItemProps = {
|
||||
attribute: ProductAttribute;
|
||||
editLabel?: string;
|
||||
removeLabel?: string;
|
||||
onDragStart?: DragEventHandler< HTMLDivElement >;
|
||||
onDragEnd?: DragEventHandler< HTMLDivElement >;
|
||||
onEditClick?: ( attribute: ProductAttribute ) => void;
|
||||
onRemoveClick?: ( attribute: ProductAttribute ) => void;
|
||||
};
|
||||
|
||||
export const AttributeListItem: React.FC< AttributeListItemProps > = ( {
|
||||
attribute,
|
||||
editLabel = __( 'edit', 'woocommerce' ),
|
||||
removeLabel = __( 'Remove attribute', 'woocommerce' ),
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
onEditClick,
|
||||
onRemoveClick,
|
||||
} ) => {
|
||||
return (
|
||||
<ListItem
|
||||
className="woocommerce-attribute-list-item"
|
||||
onDragStart={ onDragStart }
|
||||
onDragEnd={ onDragEnd }
|
||||
>
|
||||
<div>{ attribute.name }</div>
|
||||
<div className="woocommerce-attribute-list-item__options">
|
||||
{ attribute.options.slice( 0, 2 ).map( ( option, index ) => (
|
||||
<div
|
||||
className="woocommerce-attribute-list-item__option-chip"
|
||||
key={ index }
|
||||
>
|
||||
{ option }
|
||||
</div>
|
||||
) ) }
|
||||
{ attribute.options.length > 2 && (
|
||||
<div className="woocommerce-attribute-list-item__option-chip">
|
||||
{ sprintf(
|
||||
__( '+ %i more', 'woocommerce' ),
|
||||
attribute.options.length - 2
|
||||
) }
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
<div className="woocommerce-attribute-list-item__actions">
|
||||
{ typeof onEditClick === 'function' && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={ () => onEditClick( attribute ) }
|
||||
>
|
||||
{ editLabel }
|
||||
</Button>
|
||||
) }
|
||||
{ typeof onRemoveClick === 'function' && (
|
||||
<Button
|
||||
icon={ closeSmall }
|
||||
label={ removeLabel }
|
||||
onClick={ () => onRemoveClick( attribute ) }
|
||||
></Button>
|
||||
) }
|
||||
</div>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from './add-attribute-list-item';
|
||||
export * from './attribute-list-item';
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ProductAttribute } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AttributeField } from '../attribute-field';
|
||||
|
||||
type AttributesProps = {
|
||||
value: ProductAttribute[];
|
||||
onChange: ( value: ProductAttribute[] ) => void;
|
||||
productId?: number;
|
||||
};
|
||||
|
||||
export const Attributes: React.FC< AttributesProps > = ( {
|
||||
value,
|
||||
onChange,
|
||||
productId,
|
||||
} ) => {
|
||||
return (
|
||||
<AttributeField
|
||||
attributeType="regular"
|
||||
value={ value }
|
||||
onChange={ onChange }
|
||||
productId={ productId }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './attributes';
|
|
@ -0,0 +1 @@
|
|||
export * from './options';
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { ProductAttribute } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AttributeField } from '../attribute-field';
|
||||
|
||||
type OptionsProps = {
|
||||
value: ProductAttribute[];
|
||||
onChange: ( value: ProductAttribute[] ) => void;
|
||||
productId?: number;
|
||||
};
|
||||
|
||||
export const Options: React.FC< OptionsProps > = ( {
|
||||
value,
|
||||
onChange,
|
||||
productId,
|
||||
} ) => {
|
||||
return (
|
||||
<AttributeField
|
||||
attributeType="for-variations"
|
||||
value={ value }
|
||||
onChange={ onChange }
|
||||
productId={ productId }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './single-image-field';
|
|
@ -0,0 +1,28 @@
|
|||
.woocommerce-single-image-field {
|
||||
&__gallery {
|
||||
margin-top: $gap-smaller;
|
||||
|
||||
.woocommerce-image-gallery .woocommerce-sortable {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__drop-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: calc($gap * 2) 0;
|
||||
margin-top: $gap-smaller;
|
||||
gap: calc($gap * 2);
|
||||
isolation: isolate;
|
||||
|
||||
min-height: 144px;
|
||||
|
||||
background: $white;
|
||||
border: 1px dashed $gray-700;
|
||||
border-radius: 2px;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
MediaUploader,
|
||||
ImageGallery,
|
||||
ImageGalleryItem,
|
||||
} from '@woocommerce/components';
|
||||
import { MediaItem } from '@wordpress/media-utils';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './single-image-field.scss';
|
||||
|
||||
export function SingleImageField( {
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
className,
|
||||
onChange,
|
||||
...props
|
||||
}: SingleImageFieldProps ) {
|
||||
const fieldId = id ?? uniqueId();
|
||||
|
||||
function handleChange( image?: MediaItem ) {
|
||||
if ( typeof onChange === 'function' ) {
|
||||
onChange( image );
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{ ...props }
|
||||
className={ classNames(
|
||||
'woocommerce-single-image-field',
|
||||
className
|
||||
) }
|
||||
>
|
||||
<label
|
||||
htmlFor={ fieldId }
|
||||
className="components-base-control__label woocommerce-single-image-field__label"
|
||||
>
|
||||
{ label }
|
||||
</label>
|
||||
|
||||
{ value ? (
|
||||
<div
|
||||
id={ fieldId }
|
||||
className="woocommerce-single-image-field__gallery"
|
||||
tabIndex={ -1 }
|
||||
role="region"
|
||||
>
|
||||
<ImageGallery
|
||||
onReplace={ ( { media } ) => handleChange( media ) }
|
||||
onRemove={ () => handleChange( undefined ) }
|
||||
>
|
||||
<ImageGalleryItem
|
||||
key={ value.id }
|
||||
id={ String( value.id ) }
|
||||
alt={ value.alt }
|
||||
src={ value.url }
|
||||
/>
|
||||
</ImageGallery>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
id={ fieldId }
|
||||
className="woocommerce-single-image-field__drop-zone"
|
||||
tabIndex={ -1 }
|
||||
role="region"
|
||||
>
|
||||
<MediaUploader
|
||||
onError={ () => null }
|
||||
onSelect={ ( image ) =>
|
||||
handleChange( image as MediaItem )
|
||||
}
|
||||
onUpload={ ( [ image ] ) => handleChange( image ) }
|
||||
onFileUploadChange={ ( [ image ] ) =>
|
||||
handleChange( image )
|
||||
}
|
||||
label={ __(
|
||||
'Drag image here or click to upload',
|
||||
'woocommerce'
|
||||
) }
|
||||
buttonText={ __( 'Choose image', 'woocommerce' ) }
|
||||
/>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type SingleImageFieldProps = Omit<
|
||||
React.DetailedHTMLProps<
|
||||
React.HTMLAttributes< HTMLDivElement >,
|
||||
HTMLDivElement
|
||||
>,
|
||||
'onChange'
|
||||
> & {
|
||||
label: string;
|
||||
value?: MediaItem;
|
||||
onChange?( value?: MediaItem ): void;
|
||||
};
|
|
@ -1,5 +1,9 @@
|
|||
.woocommerce-product-variations {
|
||||
min-height: 300px;
|
||||
ol {
|
||||
@media ( min-width: #{ ($break-medium) } ) {
|
||||
min-height: 420px;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> div {
|
||||
|
@ -10,7 +14,7 @@
|
|||
|
||||
&__header {
|
||||
display: grid;
|
||||
grid-template-columns: calc(38px + 25%) 25% 25%;
|
||||
grid-template-columns: auto 25% 25% 88px;
|
||||
padding: $gap-small $gap;
|
||||
|
||||
h4 {
|
||||
|
@ -22,12 +26,63 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__status-dot {
|
||||
margin-right: $gap-smaller;
|
||||
&.green {
|
||||
color: $alert-green;
|
||||
}
|
||||
&.yellow {
|
||||
color: $alert-yellow;
|
||||
}
|
||||
&.red {
|
||||
color: $alert-red;
|
||||
}
|
||||
}
|
||||
|
||||
&__price--fade,
|
||||
&__quantity--fade {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
|
||||
.components-button {
|
||||
position: relative;
|
||||
color: var(--wp-admin-theme-color);
|
||||
|
||||
&:disabled,
|
||||
&[aria-disabled='true'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.components-spinner {
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.components-button svg {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.components-button--visible {
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.components-button--hidden {
|
||||
color: $alert-red;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-list-item {
|
||||
display: grid;
|
||||
grid-template-columns: 38px 25% 25% 25%;
|
||||
grid-template-columns: 38px auto 25% 25% 88px;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
margin-bottom: -1px;
|
||||
min-height: 84px;
|
||||
}
|
||||
|
||||
.woocommerce-sortable {
|
||||
|
@ -35,7 +90,7 @@
|
|||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.components-spinner {
|
||||
&.is-loading .components-spinner {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
left: 50%;
|
||||
|
|
|
@ -2,21 +2,37 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Card, Spinner } from '@wordpress/components';
|
||||
import { Button, Card, Spinner, Tooltip } from '@wordpress/components';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||
ProductVariation,
|
||||
} from '@woocommerce/data';
|
||||
import { ListItem, Pagination, Sortable, Tag } from '@woocommerce/components';
|
||||
import {
|
||||
Link,
|
||||
ListItem,
|
||||
Pagination,
|
||||
Sortable,
|
||||
Tag,
|
||||
} from '@woocommerce/components';
|
||||
import { getNewPath } from '@woocommerce/navigation';
|
||||
import { useContext, useState } from '@wordpress/element';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import classnames from 'classnames';
|
||||
import truncate from 'lodash/truncate';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { PRODUCT_VARIATION_TITLE_LIMIT } from '~/products/constants';
|
||||
import useVariationsOrder from '~/products/hooks/use-variations-order';
|
||||
import HiddenIcon from '~/products/images/hidden-icon';
|
||||
import VisibleIcon from '~/products/images/visible-icon';
|
||||
import { CurrencyContext } from '../../../lib/currency-context';
|
||||
import { getProductStockStatus } from '../../utils/get-product-stock-status';
|
||||
import {
|
||||
getProductStockStatus,
|
||||
getProductStockStatusClass,
|
||||
} from '../../utils/get-product-stock-status';
|
||||
import './variations.scss';
|
||||
|
||||
/**
|
||||
|
@ -29,9 +45,16 @@ import './variations.scss';
|
|||
*/
|
||||
const DEFAULT_PER_PAGE_OPTION = 25;
|
||||
|
||||
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
|
||||
const VISIBLE_TEXT = __( 'Visible to customers', 'woocommerce' );
|
||||
const UPDATING_TEXT = __( 'Updating product variation', 'woocommerce' );
|
||||
|
||||
export const Variations: React.FC = () => {
|
||||
const [ currentPage, setCurrentPage ] = useState( 1 );
|
||||
const [ perPage, setPerPage ] = useState( DEFAULT_PER_PAGE_OPTION );
|
||||
const [ isUpdating, setIsUpdating ] = useState< Record< string, boolean > >(
|
||||
{}
|
||||
);
|
||||
const { productId } = useParams();
|
||||
const context = useContext( CurrencyContext );
|
||||
const { formatAmount, getCurrencyConfig } = context;
|
||||
|
@ -46,6 +69,8 @@ export const Variations: React.FC = () => {
|
|||
product_id: productId,
|
||||
page: currentPage,
|
||||
per_page: perPage,
|
||||
order: 'asc',
|
||||
orderby: 'menu_order',
|
||||
};
|
||||
return {
|
||||
isLoading: ! hasFinishedResolution( 'getProductVariations', [
|
||||
|
@ -60,6 +85,13 @@ export const Variations: React.FC = () => {
|
|||
[ currentPage, perPage ]
|
||||
);
|
||||
|
||||
const { updateProductVariation } = useDispatch(
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
);
|
||||
|
||||
const { sortedVariations, getVariationKey, onOrderChange } =
|
||||
useVariationsOrder( { variations, currentPage } );
|
||||
|
||||
if ( ! variations || isLoading ) {
|
||||
return (
|
||||
<Card className="woocommerce-product-variations is-loading">
|
||||
|
@ -70,6 +102,26 @@ export const Variations: React.FC = () => {
|
|||
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
|
||||
function handleCustomerVisibilityClick(
|
||||
variationId: number,
|
||||
status: 'private' | 'publish'
|
||||
) {
|
||||
if ( isUpdating[ variationId ] ) return;
|
||||
setIsUpdating( ( prevState ) => ( {
|
||||
...prevState,
|
||||
[ variationId ]: true,
|
||||
} ) );
|
||||
updateProductVariation< Promise< ProductVariation > >(
|
||||
{ product_id: productId, id: variationId },
|
||||
{ status }
|
||||
).finally( () =>
|
||||
setIsUpdating( ( prevState ) => ( {
|
||||
...prevState,
|
||||
[ variationId ]: false,
|
||||
} ) )
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="woocommerce-product-variations">
|
||||
<div className="woocommerce-product-variations__header">
|
||||
|
@ -83,27 +135,143 @@ export const Variations: React.FC = () => {
|
|||
</h4>
|
||||
<h4>{ __( 'Quantity', 'woocommerce' ) }</h4>
|
||||
</div>
|
||||
<Sortable>
|
||||
{ variations.map( ( variation ) => (
|
||||
<ListItem key={ variation.id }>
|
||||
<Sortable onOrderChange={ onOrderChange }>
|
||||
{ sortedVariations.map( ( variation ) => (
|
||||
<ListItem key={ getVariationKey( variation ) }>
|
||||
<div className="woocommerce-product-variations__attributes">
|
||||
{ variation.attributes.map( ( attribute ) => (
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
/* @ts-ignore Additional props are not required. */
|
||||
<Tag
|
||||
id={ attribute.id }
|
||||
className="woocommerce-product-variations__attribute"
|
||||
key={ attribute.id }
|
||||
label={ attribute.option }
|
||||
/>
|
||||
) ) }
|
||||
{ variation.attributes.map( ( attribute ) => {
|
||||
const tag = (
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
/* @ts-ignore Additional props are not required. */
|
||||
<Tag
|
||||
id={ attribute.id }
|
||||
className="woocommerce-product-variations__attribute"
|
||||
key={ attribute.id }
|
||||
label={ truncate( attribute.option, {
|
||||
length: PRODUCT_VARIATION_TITLE_LIMIT,
|
||||
} ) }
|
||||
screenReaderLabel={ attribute.option }
|
||||
/>
|
||||
);
|
||||
|
||||
return attribute.option.length <=
|
||||
PRODUCT_VARIATION_TITLE_LIMIT ? (
|
||||
tag
|
||||
) : (
|
||||
<Tooltip
|
||||
key={ attribute.id }
|
||||
text={ attribute.option }
|
||||
position="top center"
|
||||
>
|
||||
<span>{ tag }</span>
|
||||
</Tooltip>
|
||||
);
|
||||
} ) }
|
||||
</div>
|
||||
<div className="woocommerce-product-variations__price">
|
||||
<div
|
||||
className={ classnames(
|
||||
'woocommerce-product-variations__price',
|
||||
{
|
||||
'woocommerce-product-variations__price--fade':
|
||||
variation.status === 'private',
|
||||
}
|
||||
) }
|
||||
>
|
||||
{ formatAmount( variation.price ) }
|
||||
</div>
|
||||
<div className="woocommerce-product-variations__quantity">
|
||||
<div
|
||||
className={ classnames(
|
||||
'woocommerce-product-variations__quantity',
|
||||
{
|
||||
'woocommerce-product-variations__quantity--fade':
|
||||
variation.status === 'private',
|
||||
}
|
||||
) }
|
||||
>
|
||||
<span
|
||||
className={ classnames(
|
||||
'woocommerce-product-variations__status-dot',
|
||||
getProductStockStatusClass( variation )
|
||||
) }
|
||||
>
|
||||
●
|
||||
</span>
|
||||
{ getProductStockStatus( variation ) }
|
||||
</div>
|
||||
<div className="woocommerce-product-variations__actions">
|
||||
<Link
|
||||
href={ getNewPath(
|
||||
{},
|
||||
`/product/${ productId }/variation/${ variation.id }`
|
||||
) }
|
||||
type="wc-admin"
|
||||
className="components-button"
|
||||
>
|
||||
{ __( 'Edit', 'woocommerce' ) }
|
||||
</Link>
|
||||
|
||||
{ variation.status === 'private' && (
|
||||
<Tooltip
|
||||
position="top center"
|
||||
text={ NOT_VISIBLE_TEXT }
|
||||
>
|
||||
<Button
|
||||
className="components-button--hidden"
|
||||
aria-label={
|
||||
isUpdating[ variation.id ]
|
||||
? UPDATING_TEXT
|
||||
: NOT_VISIBLE_TEXT
|
||||
}
|
||||
aria-disabled={
|
||||
isUpdating[ variation.id ]
|
||||
}
|
||||
onClick={ () =>
|
||||
handleCustomerVisibilityClick(
|
||||
variation.id,
|
||||
'publish'
|
||||
)
|
||||
}
|
||||
>
|
||||
{ isUpdating[ variation.id ] ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<HiddenIcon />
|
||||
) }
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) }
|
||||
|
||||
{ variation.status === 'publish' && (
|
||||
<Tooltip
|
||||
position="top center"
|
||||
text={ VISIBLE_TEXT }
|
||||
>
|
||||
<Button
|
||||
className="components-button--visible"
|
||||
aria-label={
|
||||
isUpdating[ variation.id ]
|
||||
? UPDATING_TEXT
|
||||
: VISIBLE_TEXT
|
||||
}
|
||||
aria-disabled={
|
||||
isUpdating[ variation.id ]
|
||||
}
|
||||
onClick={ () =>
|
||||
handleCustomerVisibilityClick(
|
||||
variation.id,
|
||||
'private'
|
||||
)
|
||||
}
|
||||
>
|
||||
{ isUpdating[ variation.id ] ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<VisibleIcon />
|
||||
) }
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) }
|
||||
</div>
|
||||
</ListItem>
|
||||
) ) }
|
||||
</Sortable>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { PartialProduct, ProductVariation } from '@woocommerce/data';
|
||||
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { PostsNavigationProps } from '../shared/posts-navigation';
|
||||
|
||||
export default function useProductVariationNavigation( {
|
||||
product,
|
||||
productVariation,
|
||||
}: UseProductVariationNavigationInput ): UseProductVariationNavigationOutput {
|
||||
const { variations } = product;
|
||||
const variationIds = variations ?? [];
|
||||
|
||||
const currentIndex = variationIds.indexOf( productVariation.id ?? -1 );
|
||||
const canNavigatePrev = currentIndex > 0;
|
||||
const canNavigateNext = currentIndex < variationIds.length - 1;
|
||||
const prevVariationId = canNavigatePrev
|
||||
? variationIds[ currentIndex - 1 ]
|
||||
: undefined;
|
||||
const nextVariationId = canNavigateNext
|
||||
? variationIds[ currentIndex + 1 ]
|
||||
: undefined;
|
||||
|
||||
const persistedQuery = getPersistedQuery();
|
||||
|
||||
return {
|
||||
actionHref: getNewPath( persistedQuery, `/product/${ product.id }` ),
|
||||
prevHref: prevVariationId
|
||||
? getNewPath(
|
||||
persistedQuery,
|
||||
`/product/${ product.id }/variation/${ prevVariationId }`
|
||||
)
|
||||
: undefined,
|
||||
nextHref: nextVariationId
|
||||
? getNewPath(
|
||||
persistedQuery,
|
||||
`/product/${ product.id }/variation/${ nextVariationId }`
|
||||
)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseProductVariationNavigationInput = {
|
||||
product: PartialProduct;
|
||||
productVariation: Partial< ProductVariation >;
|
||||
};
|
||||
|
||||
export type UseProductVariationNavigationOutput = Pick<
|
||||
PostsNavigationProps,
|
||||
'actionHref' | 'prevHref' | 'nextHref'
|
||||
>;
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import type { ProductVariation } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
||||
const KEY_SEPARATOR = ':';
|
||||
|
||||
function getVariationKey( variation: ProductVariation ) {
|
||||
return `${ variation.id }${ KEY_SEPARATOR }${ variation.menu_order }`;
|
||||
}
|
||||
|
||||
function getVariationId( { key }: JSX.Element ) {
|
||||
return typeof key === 'string'
|
||||
? Number.parseInt( key.split( KEY_SEPARATOR )[ 0 ], 10 )
|
||||
: 0;
|
||||
}
|
||||
|
||||
function getVariationOrder( { key }: JSX.Element ) {
|
||||
return typeof key === 'string'
|
||||
? Number.parseInt( key.split( KEY_SEPARATOR )[ 1 ], 10 )
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
function sort(
|
||||
variations: ProductVariation[],
|
||||
currentPage: number,
|
||||
{ variationsOrder }: ProductVariationsOrder
|
||||
) {
|
||||
if ( ! variationsOrder || ! variationsOrder[ currentPage ] )
|
||||
return variations;
|
||||
|
||||
const currentPageVariationsOrder = variationsOrder[ currentPage ];
|
||||
|
||||
return [ ...variations ].sort( ( a, b ) => {
|
||||
if (
|
||||
! currentPageVariationsOrder[ a.id ] ||
|
||||
! currentPageVariationsOrder[ b.id ]
|
||||
)
|
||||
return 0;
|
||||
return (
|
||||
currentPageVariationsOrder[ a.id ] -
|
||||
currentPageVariationsOrder[ b.id ]
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
export default function useVariationsOrder( {
|
||||
variations,
|
||||
currentPage,
|
||||
}: UseVariationsOrderInput ): UseVariationsOrderOutput {
|
||||
const { setValue, values } = useFormContext< ProductVariationsOrder >();
|
||||
|
||||
function onOrderChange( items: JSX.Element[] ) {
|
||||
const minOrder = Math.min( ...items.map( getVariationOrder ) );
|
||||
|
||||
setValue( 'variationsOrder', {
|
||||
...values.variationsOrder,
|
||||
[ currentPage ]: items.reduce( ( prev, item, index ) => {
|
||||
const id = getVariationId( item );
|
||||
return {
|
||||
...prev,
|
||||
[ id ]: minOrder + index,
|
||||
};
|
||||
}, {} ),
|
||||
} );
|
||||
}
|
||||
|
||||
return {
|
||||
sortedVariations: sort( variations, currentPage, values ),
|
||||
getVariationKey,
|
||||
onOrderChange,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseVariationsOrderInput = {
|
||||
variations: ProductVariation[];
|
||||
currentPage: number;
|
||||
};
|
||||
|
||||
export type UseVariationsOrderOutput = {
|
||||
sortedVariations: ProductVariation[];
|
||||
getVariationKey( variation: ProductVariation ): string;
|
||||
onOrderChange( items: JSX.Element[] ): void;
|
||||
};
|
||||
|
||||
export type ProductVariationsOrder = {
|
||||
variationsOrder?: {
|
||||
[ page: number ]: {
|
||||
[ variationId: number ]: number;
|
||||
};
|
||||
};
|
||||
};
|
|
@ -4,14 +4,13 @@ export const FeedbackIcon = () => {
|
|||
width="16"
|
||||
height="17"
|
||||
viewBox="0 0 16 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.68822 12.625L1.5 13.8145L1.5 1.5L14.5 1.5L14.5 12.625L2.68822 12.625ZM3.31 14.125L15 14.125C15.5523 14.125 16 13.6773 16 13.125L16 1C16 0.447717 15.5523 0 15 0H1C0.447717 0 0 0.447716 0 1V15.5247C0 15.8173 0.161234 16.086 0.419354 16.2237C0.727111 16.3878 1.10601 16.3313 1.35252 16.0845L3.31 14.125ZM12 5.99997H4V4.49997H12V5.99997ZM4 9.99997H9V8.49997H4V9.99997Z"
|
||||
fill="#1E1E1E"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
export default function HiddenIcon( {
|
||||
width = 24,
|
||||
height = 24,
|
||||
...props
|
||||
}: React.SVGProps< SVGSVGElement > ) {
|
||||
return (
|
||||
<svg
|
||||
{ ...props }
|
||||
width={ width }
|
||||
height={ height }
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.7226 6.2125C13.1641 6.0766 12.5883 6 11.9999 6C8.10055 6 4.75407 9.36447 3.31899 11.0546C2.8507 11.6061 2.8507 12.3939 3.31899 12.9454C4.17896 13.9582 5.72533 15.5723 7.66574 16.7033L8.41572 15.4043C8.13761 15.242 7.86389 15.0655 7.59553 14.8776C6.25019 13.9359 5.15775 12.7905 4.48406 12C5.15775 11.2095 6.25019 10.0641 7.59553 9.12235C8.96667 8.16257 10.4775 7.5 11.9999 7.5C12.3118 7.5 12.6231 7.5278 12.9329 7.58027L13.7226 6.2125ZM12.3504 8.58923C12.2352 8.57753 12.1182 8.57153 11.9999 8.57153C10.1063 8.57153 8.57132 10.1066 8.57132 12.0001C8.57132 12.7505 8.81237 13.4445 9.22126 14.0091L10.1233 12.4467C10.0893 12.3034 10.0713 12.1538 10.0713 12.0001C10.0713 11.1266 10.652 10.3888 11.4484 10.1515L12.3504 8.58923ZM12.8092 10.2491L13.5611 8.94679C14.6697 9.51479 15.4285 10.6688 15.4285 12.0001C15.4285 13.8937 13.8934 15.4287 11.9999 15.4287C11.3128 15.4287 10.6729 15.2266 10.1364 14.8785L10.8883 13.5763C11.2025 13.7983 11.5859 13.9287 11.9999 13.9287C13.065 13.9287 13.9285 13.0652 13.9285 12.0001C13.9285 11.224 13.4701 10.555 12.8092 10.2491ZM9.51376 15.957C10.3246 16.2986 11.1605 16.5 11.9999 16.5C13.5223 16.5 15.0331 15.8374 16.4043 14.8776C17.7496 13.9359 18.842 12.7905 19.5157 12C18.842 11.2095 17.7496 10.0641 16.4043 9.12235C15.6875 8.62066 14.9327 8.20018 14.1579 7.91308L14.917 6.59839C17.5164 7.64275 19.6204 9.80575 20.6808 11.0546C21.1491 11.6061 21.1491 12.3939 20.6808 12.9454C19.2457 14.6355 15.8992 18 11.9999 18C10.8611 18 9.76945 17.713 8.7588 17.2646L9.51376 15.957Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x="16.0625"
|
||||
y="4.61377"
|
||||
width="1.22727"
|
||||
height="16"
|
||||
transform="rotate(30 16.0625 4.61377)"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
export default function VisibleIcon( {
|
||||
width = 24,
|
||||
height = 24,
|
||||
...props
|
||||
}: React.SVGProps< SVGSVGElement > ) {
|
||||
return (
|
||||
<svg
|
||||
{ ...props }
|
||||
width={ width }
|
||||
height={ height }
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M20.1091 11.54C20.3396 11.8116 20.3396 12.1884 20.1091 12.46C19.4144 13.2781 18.266 14.4899 16.8343 15.4921C15.397 16.4982 13.7359 17.25 11.9999 17.25C10.2638 17.25 8.60268 16.4982 7.1654 15.4921C5.73376 14.4899 4.58533 13.2781 3.89066 12.46C3.6601 12.1884 3.6601 11.8116 3.89066 11.54C4.58533 10.7219 5.73376 9.51006 7.1654 8.50792C8.60268 7.50184 10.2638 6.75 11.9999 6.75C13.7359 6.75 15.397 7.50184 16.8343 8.50792C18.266 9.51006 19.4144 10.7219 20.1091 11.54Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle
|
||||
cx="11.9999"
|
||||
cy="11.9999"
|
||||
r="2.67857"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -62,6 +62,22 @@ $product-form-tabs-height: 56px;
|
|||
font-weight: 600;
|
||||
box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) transparent, inset 0 -3px 0 0 var(--wp-admin-theme-color);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[aria-disabled='true'] {
|
||||
// We need tooltips at full opacity so only child elements have reduced opacity.
|
||||
opacity: 1;
|
||||
|
||||
.woocommerce-product-form-tab__item-inner-text {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-product-form-tab__item-inner {
|
||||
min-height: $product-form-tabs-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Children, useEffect } from '@wordpress/element';
|
||||
import { TabPanel } from '@wordpress/components';
|
||||
import { TabPanel, Tooltip } from '@wordpress/components';
|
||||
import { navigateTo, getNewPath, getQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -14,6 +15,8 @@ import { ProductFormTab } from '../product-form-tab';
|
|||
export const ProductFormLayout: React.FC< {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
} > = ( { children } ) => {
|
||||
const query = getQuery() as Record< string, string >;
|
||||
|
||||
useEffect( () => {
|
||||
window.document.body.classList.add(
|
||||
'woocommerce-admin-product-layout'
|
||||
|
@ -32,7 +35,27 @@ export const ProductFormLayout: React.FC< {
|
|||
}
|
||||
return {
|
||||
name: child.props.name,
|
||||
title: child.props.title,
|
||||
title: child.props.disabled ? (
|
||||
<Tooltip
|
||||
text={ __(
|
||||
'Manage individual variation details in the Options tab.',
|
||||
'woocommerce'
|
||||
) }
|
||||
>
|
||||
<span className="woocommerce-product-form-tab__item-inner">
|
||||
<span className="woocommerce-product-form-tab__item-inner-text">
|
||||
{ child.props.title }
|
||||
</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="woocommerce-product-form-tab__item-inner">
|
||||
<span className="woocommerce-product-form-tab__item-inner-text">
|
||||
{ child.props.title }
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
disabled: child.props.disabled,
|
||||
};
|
||||
} );
|
||||
|
||||
|
@ -40,8 +63,16 @@ export const ProductFormLayout: React.FC< {
|
|||
<TabPanel
|
||||
className="product-form-layout"
|
||||
activeClass="is-active"
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Disabled properties will be included in newer versions of Gutenberg.
|
||||
tabs={ tabs }
|
||||
onSelect={ () => ( window.document.documentElement.scrollTop = 0 ) }
|
||||
initialTabName={ query.tab ?? tabs[ 0 ].name }
|
||||
onSelect={ ( tabName: string ) => {
|
||||
window.document.documentElement.scrollTop = 0;
|
||||
navigateTo( {
|
||||
url: getNewPath( { tab: tabName } ),
|
||||
} );
|
||||
} }
|
||||
>
|
||||
{ ( tab ) => (
|
||||
<>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ProductVariationFormActions } from '../product-variation-form-actions';
|
||||
import { ProductTitle } from '../product-title';
|
||||
|
||||
export const ProductVariationFormHeader: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ProductTitle />
|
||||
<ProductVariationFormActions />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -6,7 +6,7 @@ $gutenberg-blue-darker: var(--wp-admin-theme-color-darker-20);
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: $gap-smaller;
|
||||
padding-right: $gap-large;
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
position: fixed;
|
||||
|
@ -58,4 +58,8 @@ $gutenberg-blue-darker: var(--wp-admin-theme-color-darker-20);
|
|||
.woocommerce-layout {
|
||||
margin-bottom: calc(70px + $gap); // Product actions height + gap.
|
||||
}
|
||||
|
||||
.is-variation .woocommerce-product-form-actions__preview {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import classnames from 'classnames';
|
||||
|
||||
export const ProductFormTab: React.FC< {
|
||||
disabled?: boolean;
|
||||
name: string;
|
||||
title: string;
|
||||
children: JSX.Element | JSX.Element[] | string;
|
||||
|
|
|
@ -16,9 +16,9 @@ import { PricingSection } from './sections/pricing-section';
|
|||
import { ProductShippingSection } from './sections/product-shipping-section';
|
||||
import { ProductVariationsSection } from './sections/product-variations-section';
|
||||
import { ImagesSection } from './sections/images-section';
|
||||
import './product-page.scss';
|
||||
import { validate } from './product-validation';
|
||||
import { AttributesSection } from './sections/attributes-section';
|
||||
import { OptionsSection } from './sections/options-section';
|
||||
import { ProductFormFooter } from './layout/product-form-footer';
|
||||
import { ProductFormTab } from './product-form-tab';
|
||||
|
||||
|
@ -48,16 +48,29 @@ export const ProductForm: React.FC< {
|
|||
<ImagesSection />
|
||||
<AttributesSection />
|
||||
</ProductFormTab>
|
||||
<ProductFormTab name="pricing" title="Pricing">
|
||||
<ProductFormTab
|
||||
name="pricing"
|
||||
title="Pricing"
|
||||
disabled={ !! product?.variations?.length }
|
||||
>
|
||||
<PricingSection />
|
||||
</ProductFormTab>
|
||||
<ProductFormTab name="inventory" title="Inventory">
|
||||
<ProductFormTab
|
||||
name="inventory"
|
||||
title="Inventory"
|
||||
disabled={ !! product?.variations?.length }
|
||||
>
|
||||
<ProductInventorySection />
|
||||
</ProductFormTab>
|
||||
<ProductFormTab name="shipping" title="Shipping">
|
||||
<ProductFormTab
|
||||
name="shipping"
|
||||
title="Shipping"
|
||||
disabled={ !! product?.variations?.length }
|
||||
>
|
||||
<ProductShippingSection product={ product } />
|
||||
</ProductFormTab>
|
||||
<ProductFormTab name="options" title="Options">
|
||||
<OptionsSection />
|
||||
<ProductVariationsSection />
|
||||
</ProductFormTab>
|
||||
</ProductFormLayout>
|
||||
|
|
|
@ -38,11 +38,10 @@ export const ProductMoreMenu = () => {
|
|||
<>
|
||||
<MenuItem
|
||||
onClick={ () => {
|
||||
// @todo This should open the CES modal.
|
||||
showCesModal(
|
||||
{
|
||||
action: 'new_product',
|
||||
label: __(
|
||||
title: __(
|
||||
"How's your experience with the product editor?",
|
||||
'woocommerce'
|
||||
),
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.woocommerce-product-settings__toggle {
|
||||
margin-left: -$gap;
|
||||
}
|
||||
|
||||
@include breakpoint( '<782px' ) {
|
||||
.woocommerce-product-settings__toggle,
|
||||
.woocommerce-product-settings__panel {
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||
Product,
|
||||
PRODUCTS_STORE_NAME,
|
||||
WCDataSelector,
|
||||
} from '@woocommerce/data';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
import { getNewPath } from '@woocommerce/navigation';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
|
@ -16,6 +18,10 @@ import { useSelect } from '@wordpress/data';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { getProductTitle } from './utils/get-product-title';
|
||||
import {
|
||||
getProductVariationTitle,
|
||||
getTruncatedProductVariationTitle,
|
||||
} from './utils/get-product-variation-title';
|
||||
import { ProductBreadcrumbs } from './product-breadcrumbs';
|
||||
import { ProductStatusBadge } from './product-status-badge';
|
||||
import { WooHeaderPageTitle } from '~/header/utils';
|
||||
|
@ -23,35 +29,102 @@ import './product-title.scss';
|
|||
|
||||
export const ProductTitle: React.FC = () => {
|
||||
const { values } = useFormContext< Product >();
|
||||
const { productId } = useParams();
|
||||
const { persistedName } = useSelect( ( select: WCDataSelector ) => {
|
||||
const product = productId
|
||||
? select( PRODUCTS_STORE_NAME ).getProduct(
|
||||
const { productId, variationId } = useParams();
|
||||
const { isLoading, persistedName, productVariation } = useSelect(
|
||||
( select: WCDataSelector ) => {
|
||||
const {
|
||||
getProduct,
|
||||
hasFinishedResolution: hasProductFinishedResolution,
|
||||
} = select( PRODUCTS_STORE_NAME );
|
||||
const {
|
||||
getProductVariation,
|
||||
hasFinishedResolution: hasProductVariationFinishedResolution,
|
||||
} = select( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
|
||||
const product = productId
|
||||
? getProduct( parseInt( productId, 10 ) )
|
||||
: null;
|
||||
const variation =
|
||||
variationId && productId
|
||||
? getProductVariation( {
|
||||
id: parseInt( variationId, 10 ),
|
||||
product_id: parseInt( productId, 10 ),
|
||||
} )
|
||||
: null;
|
||||
|
||||
const isProductLoading =
|
||||
productId &&
|
||||
! hasProductFinishedResolution( 'getProduct', [
|
||||
parseInt( productId, 10 ),
|
||||
undefined
|
||||
)
|
||||
: null;
|
||||
] );
|
||||
|
||||
return {
|
||||
persistedName: product?.name,
|
||||
};
|
||||
} );
|
||||
const isVariationLoading =
|
||||
variationId &&
|
||||
productId &&
|
||||
! hasProductVariationFinishedResolution(
|
||||
'getProductVariation',
|
||||
[
|
||||
{
|
||||
id: parseInt( variationId, 10 ),
|
||||
product_id: parseInt( productId, 10 ),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const breadcrumbs = [
|
||||
return {
|
||||
persistedName: product?.name,
|
||||
productVariation: variation,
|
||||
isLoading: isProductLoading || isVariationLoading,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const productTitle = getProductTitle(
|
||||
values.name,
|
||||
values.type,
|
||||
persistedName
|
||||
);
|
||||
const productVariationTitle =
|
||||
productVariation && getProductVariationTitle( productVariation );
|
||||
|
||||
const pageHierarchy = [
|
||||
{
|
||||
href: getAdminLink( 'edit.php?post_type=product' ),
|
||||
title: __( 'Products', 'woocommerce' ),
|
||||
},
|
||||
];
|
||||
const title = getProductTitle( values.name, values.type, persistedName );
|
||||
{
|
||||
href: getNewPath( {}, '/product/' + productId ),
|
||||
type: 'wc-admin',
|
||||
title: (
|
||||
<>
|
||||
{ productTitle }
|
||||
<ProductStatusBadge />
|
||||
</>
|
||||
),
|
||||
},
|
||||
productVariationTitle && {
|
||||
title: (
|
||||
<span title={ productVariationTitle }>
|
||||
{ getTruncatedProductVariationTitle( productVariation ) }
|
||||
</span>
|
||||
),
|
||||
},
|
||||
].filter( ( page ) => !! page ) as {
|
||||
href: string;
|
||||
title: string | JSX.Element;
|
||||
}[];
|
||||
|
||||
const current = pageHierarchy.pop();
|
||||
|
||||
if ( isLoading ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WooHeaderPageTitle>
|
||||
<span className="woocommerce-product-title">
|
||||
<ProductBreadcrumbs breadcrumbs={ breadcrumbs } />
|
||||
<ProductBreadcrumbs breadcrumbs={ pageHierarchy } />
|
||||
<span className="woocommerce-product-title__wrapper">
|
||||
{ title }
|
||||
<ProductStatusBadge />
|
||||
{ current?.title }
|
||||
</span>
|
||||
</span>
|
||||
</WooHeaderPageTitle>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue