diff --git a/.github/workflows/autotagger.yml b/.github/workflows/autotagger.yml index b41bf8e530bfe..c758a0bdbe002 100644 --- a/.github/workflows/autotagger.yml +++ b/.github/workflows/autotagger.yml @@ -70,6 +70,7 @@ jobs: export GIT_COMMITTER_NAME=matticbot export GIT_COMMITTER_EMAIL=matticbot@users.noreply.github.com + EXIT=0 echo "Creating tags..." TOPUSH=() for T in "${TAGS[@]}"; do @@ -79,8 +80,22 @@ jobs: done if [[ ${#TOPUSH[@]} -gt 0 ]]; then echo "Pushing tags..." - git push origin "${TOPUSH[@]}" - echo "::notice::Created tags: ${TOPUSH[*]}" + # GitHub has a limit on the number of tags that can be updated in a single push. So do them in batches. + DONE=() + while [[ ${#TOPUSH[@]} -gt 0 ]]; do + BATCH=( "${TOPUSH[@]:0:5}" ) + if git push origin "${BATCH[@]}"; then + DONE+=( "${BATCH[@]}" ) + else + echo "::error::Failed to create tags: ${BATCH[*]}" + EXIT=1 + fi + TOPUSH=( "${TOPUSH[@]:5}" ) + done + if [[ ${#DONE[@]} -gt 0 ]]; then + echo "::notice::Created tags: ${DONE[*]}" + fi else echo "::notice::No tags needed creation." fi + exit $EXIT diff --git a/.github/workflows/slack-workflow-failed.yml b/.github/workflows/slack-workflow-failed.yml index 2c56f821f7d38..372ba4879536a 100644 --- a/.github/workflows/slack-workflow-failed.yml +++ b/.github/workflows/slack-workflow-failed.yml @@ -8,6 +8,7 @@ on: - Build Docker - Tests - Gardening + - Monorepo Auto-tagger - Post-Build - PR is up-to-date - Update Jetpack Staging Test Sites diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c81faf524495..efc59f7f4542d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3847,6 +3847,9 @@ importers: configstore: specifier: 5.0.1 version: 5.0.1 + enquirer: + specifier: 2.4.1 + version: 2.4.1 envfile: specifier: 6.17.0 version: 6.17.0 @@ -13790,6 +13793,11 @@ packages: allure-js-commons: 2.9.2 dev: true + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: false + /ansi-escapes@3.2.0: resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} engines: {node: '>=4'} @@ -15942,6 +15950,14 @@ packages: graceful-fs: 4.2.11 tapable: 2.2.1 + /enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + dev: false + /entities@2.1.0: resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} dev: false diff --git a/projects/github-actions/repo-gardening/README.md b/projects/github-actions/repo-gardening/README.md index 5abf17df05f21..685834691a5af 100644 --- a/projects/github-actions/repo-gardening/README.md +++ b/projects/github-actions/repo-gardening/README.md @@ -86,6 +86,7 @@ The action relies on the following parameters. - (Optional) `reply_to_customers_threshold`. It is optional, and defaults to 10. It is the minimum number of support references needed to trigger an alert that we need to reply to customers. - (Optional) `triage_projects_token` is a [personal access token](https://github.com/settings/tokens/new) with `repo` and `project` scopes. The token should be stored in a secret. This is required if you want to use the `updateBoard` task. - (Optional) `project_board_url` is the URL of a GitHub Project Board. We'll automate some of the work on that board in the `updateBoard` task. +- (Optional) `labels_team_assignments` is a list of features you can provide, with matching team names, as specified in the "Team" field of your GitHub Project Board used for the `updateBoard` task, and lists of labels in use in your repository. #### How to create a Slack bot and get your SLACK_TOKEN diff --git a/projects/github-actions/repo-gardening/action.yml b/projects/github-actions/repo-gardening/action.yml index 715f1b0c9ee36..475d88c52d098 100644 --- a/projects/github-actions/repo-gardening/action.yml +++ b/projects/github-actions/repo-gardening/action.yml @@ -52,6 +52,10 @@ inputs: description: "URL of the GitHub project board to update" required: false default: "" + labels_team_assignments: + description: "Mapping of team assignments for labels" + required: false + default: "" runs: using: node16 main: "dist/index.js" diff --git a/projects/github-actions/repo-gardening/changelog/fix-gather-issue-references-pr b/projects/github-actions/repo-gardening/changelog/fix-gather-issue-references-pr new file mode 100644 index 0000000000000..1d4167b35e56c --- /dev/null +++ b/projects/github-actions/repo-gardening/changelog/fix-gather-issue-references-pr @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Issue references: do not gather support references in Pull Requests, only in issues. diff --git a/projects/github-actions/repo-gardening/changelog/fix-gather-issue-references-pr#2 b/projects/github-actions/repo-gardening/changelog/fix-gather-issue-references-pr#2 new file mode 100644 index 0000000000000..08defba15b52f --- /dev/null +++ b/projects/github-actions/repo-gardening/changelog/fix-gather-issue-references-pr#2 @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Issue references: avoid changing capitalization of p2 shortlinks. diff --git a/projects/github-actions/repo-gardening/changelog/fix-update-board-trigger-reference b/projects/github-actions/repo-gardening/changelog/fix-update-board-trigger-reference new file mode 100644 index 0000000000000..72fa1f6354f63 --- /dev/null +++ b/projects/github-actions/repo-gardening/changelog/fix-update-board-trigger-reference @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Automated Board triage: fix event reference to trigger the action. diff --git a/projects/github-actions/repo-gardening/changelog/update-repo-gardening-update-board-team b/projects/github-actions/repo-gardening/changelog/update-repo-gardening-update-board-team new file mode 100644 index 0000000000000..1bbba2c2af44f --- /dev/null +++ b/projects/github-actions/repo-gardening/changelog/update-repo-gardening-update-board-team @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Project Board triage: automatically assign teams to an issue based on issue labels. diff --git a/projects/github-actions/repo-gardening/changelog/update-support-reference-matches b/projects/github-actions/repo-gardening/changelog/update-support-reference-matches new file mode 100644 index 0000000000000..6f956509b2ba7 --- /dev/null +++ b/projects/github-actions/repo-gardening/changelog/update-support-reference-matches @@ -0,0 +1,5 @@ +Significance: patch +Type: changed +Comment: Issue references triage: update matching for p2 comments + + diff --git a/projects/github-actions/repo-gardening/src/index.js b/projects/github-actions/repo-gardening/src/index.js index 40a43db254575..807c60525f8cd 100644 --- a/projects/github-actions/repo-gardening/src/index.js +++ b/projects/github-actions/repo-gardening/src/index.js @@ -89,7 +89,7 @@ const automations = [ }, { event: 'issues', - action: [ 'labeled' ], + action: [ 'labeled', 'opened' ], task: updateBoard, }, ]; diff --git a/projects/github-actions/repo-gardening/src/tasks/check-description/index.js b/projects/github-actions/repo-gardening/src/tasks/check-description/index.js index c838c95cc5ce6..42571b8ef60b7 100644 --- a/projects/github-actions/repo-gardening/src/tasks/check-description/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/check-description/index.js @@ -5,10 +5,10 @@ const debug = require( '../../utils/debug' ); const getAffectedChangeloggerProjects = require( '../../utils/get-affected-changelogger-projects' ); const getComments = require( '../../utils/get-comments' ); const getFiles = require( '../../utils/get-files' ); -const getLabels = require( '../../utils/get-labels' ); const getNextValidMilestone = require( '../../utils/get-next-valid-milestone' ); const getPluginNames = require( '../../utils/get-plugin-names' ); const getPrWorkspace = require( '../../utils/get-pr-workspace' ); +const getLabels = require( '../../utils/labels/get-labels' ); /* global GitHub, WebhookPayloadPullRequest */ diff --git a/projects/github-actions/repo-gardening/src/tasks/clean-labels/index.js b/projects/github-actions/repo-gardening/src/tasks/clean-labels/index.js index 747628ef911cd..1b98b5ef961ce 100644 --- a/projects/github-actions/repo-gardening/src/tasks/clean-labels/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/clean-labels/index.js @@ -1,5 +1,5 @@ const debug = require( '../../utils/debug' ); -const getLabels = require( '../../utils/get-labels' ); +const getLabels = require( '../../utils/labels/get-labels' ); /* global GitHub, WebhookPayloadPullRequest */ diff --git a/projects/github-actions/repo-gardening/src/tasks/flag-oss/index.js b/projects/github-actions/repo-gardening/src/tasks/flag-oss/index.js index 1073806a38ba2..dbe828e2c1114 100644 --- a/projects/github-actions/repo-gardening/src/tasks/flag-oss/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/flag-oss/index.js @@ -1,6 +1,6 @@ const { getInput, setFailed } = require( '@actions/core' ); const debug = require( '../../utils/debug' ); -const sendSlackMessage = require( '../../utils/send-slack-message' ); +const sendSlackMessage = require( '../../utils/slack/send-slack-message' ); /* global GitHub, WebhookPayloadPullRequest */ @@ -27,12 +27,6 @@ async function flagOss( payload, octokit ) { labels: [ 'OSS Citizen' ], } ); - const slackToken = getInput( 'slack_token' ); - if ( ! slackToken ) { - setFailed( `flag-oss: Input slack_token is required but missing. Aborting.` ); - return; - } - const channel = getInput( 'slack_team_channel' ); if ( ! channel ) { setFailed( `flag-oss: Input slack_team_channel is required but missing. Aborting.` ); @@ -43,7 +37,6 @@ async function flagOss( payload, octokit ) { await sendSlackMessage( `An external contributor submitted this PR. Be sure to go welcome them! 👏`, channel, - slackToken, payload ); } diff --git a/projects/github-actions/repo-gardening/src/tasks/gather-support-references/index.js b/projects/github-actions/repo-gardening/src/tasks/gather-support-references/index.js index a7c8f070038a1..6480b4f2eb3a0 100644 --- a/projects/github-actions/repo-gardening/src/tasks/gather-support-references/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/gather-support-references/index.js @@ -1,8 +1,8 @@ const { getInput } = require( '@actions/core' ); const debug = require( '../../utils/debug' ); const getComments = require( '../../utils/get-comments' ); -const getLabels = require( '../../utils/get-labels' ); -const sendSlackMessage = require( '../../utils/send-slack-message' ); +const getLabels = require( '../../utils/labels/get-labels' ); +const sendSlackMessage = require( '../../utils/slack/send-slack-message' ); /* global GitHub, WebhookPayloadIssue */ @@ -87,7 +87,11 @@ async function getIssueReferences( octokit, owner, repo, number, issueComments ) const correctedId = `${ wrongId[ 1 ] }-zen`; correctedSupportIds.add( correctedId ); } else { - correctedSupportIds.add( supportId.toLowerCase() ); + // Switch to lowercase when it's not a p2 comment reference. + const standardizedsupportId = supportId.match( /[a-zA-Z0-9-]+-p2#comment-[0-9]*/ ) + ? supportId + : supportId.toLowerCase(); + correctedSupportIds.add( standardizedsupportId ); } } ); @@ -230,7 +234,7 @@ async function checkForEscalation( issueReferences, commentBody, escalationNote, ); const message = `:warning: This issue has now gathered more than 10 tickets. It may be time to reconsider its priority.`; const slackMessageFormat = formatSlackMessage( payload, channel, message ); - await sendSlackMessage( message, channel, slackToken, payload, slackMessageFormat ); + await sendSlackMessage( message, channel, payload, slackMessageFormat ); return true; } @@ -430,10 +434,20 @@ async function addHappinessLabel( octokit, ownerLogin, repo, number ) { * @param {GitHub} octokit - Initialized Octokit REST client. */ async function gatherSupportReferences( payload, octokit ) { - const { issue, repository } = payload; - const { number } = issue; + const { + issue: { number, pull_request }, + repository, + } = payload; const { name: repo, owner } = repository; + // Do not run this task on pull requests. + if ( pull_request ) { + debug( + `gather-support-references: do not gather support references on Pull Requests, here #${ number }. Aborting.` + ); + return; + } + const issueComments = await getComments( octokit, owner.login, repo, number ); const issueReferences = await getIssueReferences( octokit, owner, repo, number, issueComments ); if ( issueReferences.length > 0 ) { diff --git a/projects/github-actions/repo-gardening/src/tasks/notify-design/index.js b/projects/github-actions/repo-gardening/src/tasks/notify-design/index.js index e87795a0a9843..b05ab9bf8793d 100644 --- a/projects/github-actions/repo-gardening/src/tasks/notify-design/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/notify-design/index.js @@ -1,7 +1,7 @@ const { getInput, setFailed } = require( '@actions/core' ); const debug = require( '../../utils/debug' ); -const getLabels = require( '../../utils/get-labels' ); -const sendSlackMessage = require( '../../utils/send-slack-message' ); +const getLabels = require( '../../utils/labels/get-labels' ); +const sendSlackMessage = require( '../../utils/slack/send-slack-message' ); /* global GitHub, WebhookPayloadPullRequest */ @@ -61,12 +61,6 @@ async function notifyDesign( payload, octokit ) { const { owner, name: repo } = repository; const ownerLogin = owner.login; - const slackToken = getInput( 'slack_token' ); - if ( ! slackToken ) { - setFailed( `notify-design: Input slack_token is required but missing. Aborting.` ); - return; - } - const channel = getInput( 'slack_design_channel' ); if ( ! channel ) { setFailed( `notify-design: Input slack_design_channel is required but missing. Aborting.` ); @@ -89,7 +83,6 @@ async function notifyDesign( payload, octokit ) { await sendSlackMessage( `Someone would be interested in input from the Design team on this topic.`, channel, - slackToken, payload ); } @@ -103,7 +96,6 @@ async function notifyDesign( payload, octokit ) { await sendSlackMessage( `Someone is looking for a review from the design team.`, channel, - slackToken, payload ); } diff --git a/projects/github-actions/repo-gardening/src/tasks/notify-editorial/index.js b/projects/github-actions/repo-gardening/src/tasks/notify-editorial/index.js index 78e57e453c87e..a0d1b581fdca4 100644 --- a/projects/github-actions/repo-gardening/src/tasks/notify-editorial/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/notify-editorial/index.js @@ -1,7 +1,7 @@ const { getInput, setFailed } = require( '@actions/core' ); const debug = require( '../../utils/debug' ); -const getLabels = require( '../../utils/get-labels' ); -const sendSlackMessage = require( '../../utils/send-slack-message' ); +const getLabels = require( '../../utils/labels/get-labels' ); +const sendSlackMessage = require( '../../utils/slack/send-slack-message' ); /* global GitHub, WebhookPayloadPullRequest */ @@ -98,7 +98,6 @@ async function notifyEditorial( payload, octokit ) { await sendSlackMessage( `Someone would be interested in input from the Editorial team on this topic.`, channel, - slackToken, payload ); } @@ -112,7 +111,6 @@ async function notifyEditorial( payload, octokit ) { await sendSlackMessage( `Someone is looking for a review from the Editorial team.`, channel, - slackToken, payload ); } diff --git a/projects/github-actions/repo-gardening/src/tasks/reply-to-customers-reminder/index.js b/projects/github-actions/repo-gardening/src/tasks/reply-to-customers-reminder/index.js index 2cfd9cda4ea1f..cc25c7d4f4e1b 100644 --- a/projects/github-actions/repo-gardening/src/tasks/reply-to-customers-reminder/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/reply-to-customers-reminder/index.js @@ -1,8 +1,9 @@ const { getInput, setFailed } = require( '@actions/core' ); const debug = require( '../../utils/debug' ); const getComments = require( '../../utils/get-comments' ); -const getLabels = require( '../../utils/get-labels' ); -const sendSlackMessage = require( '../../utils/send-slack-message' ); +const getLabels = require( '../../utils/labels/get-labels' ); +const hasManySupportReferences = require( '../../utils/parse-content/has-many-support-references' ); +const sendSlackMessage = require( '../../utils/slack/send-slack-message' ); /* global GitHub, WebhookPayloadIssue */ @@ -21,35 +22,6 @@ async function hasHighPriorityLabel( octokit, owner, repo, number ) { return labels.some( label => label === '[Pri] High' || label === '[Pri] BLOCKER' ); } -/** - * Check if the issue has a comment with a list of support references, - * and at least x support references listed there. - * (x is specified with reply_to_customers_threshold input, default to 10). - * We only count the number of unanswered support references, since they're the ones we'll need to contact. - * - * @param {Array} issueComments - Array of all comments on that issue. - * @returns {Promise} Promise resolving to boolean. - */ -async function hasManySupportReferences( issueComments ) { - const referencesThreshhold = getInput( 'reply_to_customers_threshold' ); - - let isWidelySpreadIssue = false; - issueComments.map( comment => { - if ( - comment.user.login === 'github-actions[bot]' && - comment.body.includes( '**Support References**' ) - ) { - // Count the number of to-do items in the comment. - const countReferences = comment.body.split( '- [ ] ' ).length - 1; - if ( countReferences >= parseInt( referencesThreshhold ) ) { - isWidelySpreadIssue = true; - } - } - } ); - - return isWidelySpreadIssue; -} - /** * Build an object containing the slack message and its formatting to send to Slack. * @@ -136,14 +108,6 @@ async function replyToCustomersReminder( payload, octokit ) { const { full_name, owner, name: repo } = repository; const ownerLogin = owner.login; - const slackToken = getInput( 'slack_token' ); - if ( ! slackToken ) { - setFailed( - `reply-to-customers-reminder: Input slack_token is required but missing. Aborting.` - ); - return; - } - const channel = getInput( 'slack_he_triage_channel' ); if ( ! channel ) { setFailed( @@ -184,7 +148,7 @@ Before you send follow-up replies, you'll want to make sure the fix has been dep }`; const slackMessageFormat = formatSlackMessage( payload, channel, message ); - await sendSlackMessage( message, channel, slackToken, payload, slackMessageFormat ); + await sendSlackMessage( message, channel, payload, slackMessageFormat ); } module.exports = replyToCustomersReminder; diff --git a/projects/github-actions/repo-gardening/src/tasks/triage-issues/index.js b/projects/github-actions/repo-gardening/src/tasks/triage-issues/index.js index ab7eacc368805..b329e34cd58ff 100644 --- a/projects/github-actions/repo-gardening/src/tasks/triage-issues/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/triage-issues/index.js @@ -1,207 +1,16 @@ const { getInput, setFailed } = require( '@actions/core' ); const debug = require( '../../utils/debug' ); -const getLabels = require( '../../utils/get-labels' ); -const sendSlackMessage = require( '../../utils/send-slack-message' ); +const hasPriorityLabels = require( '../../utils/labels/has-priority-labels' ); +const isBug = require( '../../utils/labels/is-bug' ); +const findPlatforms = require( '../../utils/parse-content/find-platforms' ); +const findPlugins = require( '../../utils/parse-content/find-plugins' ); +const findPriority = require( '../../utils/parse-content/find-priority' ); +const formatSlackMessage = require( '../../utils/slack/format-slack-message' ); +const notifyImportantIssues = require( '../../utils/slack/notify-important-issues' ); +const sendSlackMessage = require( '../../utils/slack/send-slack-message' ); /* global GitHub, WebhookPayloadIssue */ -/** - * Check for Priority labels on an issue. - * It could be existing labels, - * or it could be that it's being added as part of the event that triggers this action. - * - * @param {GitHub} octokit - Initialized Octokit REST client. - * @param {string} owner - Repository owner. - * @param {string} repo - Repository name. - * @param {string} number - Issue number. - * @param {string} action - Action that triggered the event ('opened', 'reopened', 'labeled'). - * @param {object} eventLabel - Label that was added to the issue. - * @returns {Promise} Promise resolving to an array of Priority labels. - */ -async function hasPriorityLabels( octokit, owner, repo, number, action, eventLabel ) { - const labels = await getLabels( octokit, owner, repo, number ); - if ( 'labeled' === action && eventLabel.name && eventLabel.name.match( /^\[Pri\].*$/ ) ) { - labels.push( eventLabel.name ); - } - - return labels.filter( label => label.match( /^\[Pri\].*$/ ) ); -} - -/** - * Check for a "[Status] Priority Review Triggered" label showing that it was already escalated. - * It could be an existing label, - * or it could be that it's being added as part of the event that triggers this action. - * - * @param {GitHub} octokit - Initialized Octokit REST client. - * @param {string} owner - Repository owner. - * @param {string} repo - Repository name. - * @param {string} number - Issue number. - * @param {string} action - Action that triggered the event ('opened', 'reopened', 'labeled'). - * @param {object} eventLabel - Label that was added to the issue. - * @returns {Promise} Promise resolving to boolean. - */ -async function hasEscalatedLabel( octokit, owner, repo, number, action, eventLabel ) { - // Check for an exisiting label first. - const labels = await getLabels( octokit, owner, repo, number ); - if ( - labels.includes( '[Status] Priority Review Triggered' ) || - labels.includes( '[Status] Escalated to Kitkat' ) - ) { - return true; - } - - // If the issue is being labeled, check if the label is "[Status] Priority Review Triggered". - // No need to check for "[Status] Escalated to Kitkat" here, it's a legacy label. - if ( - 'labeled' === action && - eventLabel.name && - eventLabel.name.match( /^\[Status\] Priority Review Triggered.*$/ ) - ) { - return true; - } -} - -/** - * Ensure the issue is a bug, by looking for a "[Type] Bug" label. - * It could be an existing label, - * or it could be that it's being added as part of the event that triggers this action. - * - * @param {GitHub} octokit - Initialized Octokit REST client. - * @param {string} owner - Repository owner. - * @param {string} repo - Repository name. - * @param {string} number - Issue number. - * @param {string} action - Action that triggered the event ('opened', 'reopened', 'labeled'). - * @param {object} eventLabel - Label that was added to the issue. - * @returns {Promise} Promise resolving to boolean. - */ -async function isBug( octokit, owner, repo, number, action, eventLabel ) { - // If the issue has a "[Type] Bug" label, it's a bug. - const labels = await getLabels( octokit, owner, repo, number ); - if ( labels.includes( '[Type] Bug' ) ) { - return true; - } - - // Next, check if the current event was a [Type] Bug label being added. - if ( 'labeled' === action && eventLabel.name && '[Type] Bug' === eventLabel.name ) { - return true; - } -} - -/** - * Find list of plugins impacted by issue, based off issue contents. - * - * @param {string} body - The issue content. - * @returns {Array} Plugins concerned by issue. - */ -function findPlugins( body ) { - const regex = /###\sImpacted\splugin\n\n([a-zA-Z ,]*)\n\n/gm; - - const match = regex.exec( body ); - if ( match ) { - const [ , plugins ] = match; - return plugins.split( ', ' ).filter( v => v.trim() !== '' ); - } - - debug( `triage-issues: No plugin indicators found.` ); - return []; -} - -/** - * Find platform info, based off issue contents. - * - * @param {string} body - The issue content. - * @returns {Array} Platforms impacted by issue. - */ -function findPlatforms( body ) { - const regex = /###\sPlatform\s\(Simple\sand\/or Atomic\)\n\n([a-zA-Z ,-]*)\n\n/gm; - - const match = regex.exec( body ); - if ( match ) { - const [ , platforms ] = match; - return platforms - .split( ', ' ) - .filter( platform => platform !== 'Self-hosted' && platform.trim() !== '' ); - } - - debug( `triage-issues: no platform indicators found.` ); - return []; -} - -/** - * Figure out the priority of the issue, based off issue contents. - * Logic follows this priority matrix: pciE2j-oG-p2 - * - * @param {string} body - The issue content. - * @returns {string} Priority of issue. - */ -function findPriority( body ) { - // Look for priority indicators in body. - const priorityRegex = - /###\sImpact\n\n(?.*)\n\n###\sAvailable\sworkarounds\?\n\n(?.*)\n/gm; - let match; - while ( ( match = priorityRegex.exec( body ) ) ) { - const [ , impact = '', blocking = '' ] = match; - - debug( - `triage-issues: Reported priority indicators for issue: "${ impact }" / "${ blocking }"` - ); - - if ( blocking === 'No and the platform is unusable' ) { - return impact === 'One' ? 'High' : 'BLOCKER'; - } else if ( blocking === 'No but the platform is still usable' ) { - return 'High'; - } else if ( blocking === 'Yes, difficult to implement' ) { - return impact === 'All' ? 'High' : 'Normal'; - } else if ( blocking !== '' && blocking !== '_No response_' ) { - return impact === 'All' || impact === 'Most (> 50%)' ? 'Normal' : 'Low'; - } - return 'TBD'; - } - - debug( `triage-issues: No priority indicators found.` ); - return 'TBD'; -} - -/** - * Build an object containing the slack message and its formatting to send to Slack. - * - * @param {WebhookPayloadIssue} payload - Issue event payload. - * @param {string} channel - Slack channel ID. - * @param {string} message - Basic message (without the formatting). - * @returns {object} Object containing the slack message and its formatting. - */ -function formatSlackMessage( payload, channel, message ) { - const { issue } = payload; - const { html_url, title } = issue; - - return { - channel, - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: message, - }, - }, - { - type: 'divider', - }, - { - type: 'section', - text: { - type: 'mrkdwn', - text: `<${ html_url }|${ title }>`, - }, - }, - ], - text: `${ message } -- <${ html_url }|${ title }>`, // Fallback text for display in notifications. - mrkdwn: true, // Formatting of the fallback text. - unfurl_links: false, - unfurl_media: false, - }; -} - /** * Automatically add labels to issues, and send Slack notifications. * @@ -214,16 +23,10 @@ function formatSlackMessage( payload, channel, message ) { */ async function triageIssues( payload, octokit ) { const { action, issue, label = {}, repository } = payload; - const { number, body, state } = issue; + const { number, body } = issue; const { owner, name, full_name } = repository; const ownerLogin = owner.login; - const slackToken = getInput( 'slack_token' ); - if ( ! slackToken ) { - setFailed( 'triage-issues: Input slack_token is required but missing. Aborting.' ); - return; - } - const channel = getInput( 'slack_quality_channel' ); if ( ! channel ) { setFailed( 'triage-issues: Input slack_quality_channel is required but missing. Aborting.' ); @@ -302,55 +105,18 @@ async function triageIssues( payload, octokit ) { // send a Slack notification. if ( priority === 'TBD' && full_name === 'Automattic/wp-calypso' ) { debug( - `triage-issues: #${ number } doesn't have a Priority set. Sending in Slack message to the Kitkat team.` + `triage-issues: #${ number } doesn't have a Priority set. Sending in Slack message to the triage team.` ); const message = 'New bug missing priority. Please do a priority assessment.'; const slackMessageFormat = formatSlackMessage( payload, channel, message ); - await sendSlackMessage( message, channel, slackToken, payload, slackMessageFormat ); + await sendSlackMessage( message, channel, payload, slackMessageFormat ); } } } - /* - * Send a Slack Notification if the issue is important. - * - * We define an important issue when meeting all of the following criteria: - * - A bug (includes a "[Type] Bug" label, or a "[Type] Bug" label is added to the issue right now) - * - The issue is still opened - * - The issue is not escalated yet (no "[Status] Priority Review Triggered" label) - * - The issue is either a high priority or a blocker (inferred from the existing labels or from the issue body) - * - The issue is not already set to another priority label (no "[Pri] High", "[Pri] BLOCKER", or "[Pri] TBD" label) - */ - - const isEscalated = await hasEscalatedLabel( octokit, ownerLogin, name, number, action, label ); - - const highPriorityIssue = priority === 'High' || priorityLabels.includes( '[Pri] High' ); - const blockerIssue = priority === 'BLOCKER' || priorityLabels.includes( '[Pri] BLOCKER' ); - - const hasOtherPriorityLabels = priorityLabels.some( priLabel => - /^\[Pri\] (?!High|BLOCKER|TBD)/.test( priLabel ) - ); - - if ( - isBugIssue && - state === 'open' && - ! isEscalated && - ( highPriorityIssue || blockerIssue ) && - ! hasOtherPriorityLabels - ) { - const message = `New ${ - highPriorityIssue ? 'High-priority' : 'Blocker' - } bug! Please check the priority.`; - const slackMessageFormat = formatSlackMessage( payload, channel, message ); - await sendSlackMessage( message, channel, slackToken, payload, slackMessageFormat ); - - debug( `triage-issues: Adding a label to issue #${ number } to show that Kitkat was warned.` ); - await octokit.rest.issues.addLabels( { - owner: ownerLogin, - repo: name, - issue_number: number, - labels: [ '[Status] Priority Review Triggered' ], - } ); + // Send a Slack notification if the issue is important. + if ( isBugIssue ) { + await notifyImportantIssues( octokit, payload, channel ); } } module.exports = triageIssues; diff --git a/projects/github-actions/repo-gardening/src/tasks/update-board/automattic-label-team-assignments.js b/projects/github-actions/repo-gardening/src/tasks/update-board/automattic-label-team-assignments.js new file mode 100644 index 0000000000000..322e5249f836e --- /dev/null +++ b/projects/github-actions/repo-gardening/src/tasks/update-board/automattic-label-team-assignments.js @@ -0,0 +1,164 @@ +/** + * Map specific teams to one or more labels that may be added to issues. + * The key is a feature name. + * For each feature, we can define: + * - a team name as specified in the "Team" field of a GitHub Project Board. + * - an array of labels that this team wants to be notified about. + * - a Slack channel ID if the team wants to be notified of high/blocker priority issues in a specific Slack channel. + * - a project board ID if the team would like issues to be automatically added to a specific project board. + */ +export const automatticAssignments = { + // WordPress.com Division. + 'Blogging Prompts': { + team: 'Loop', + labels: [ '[Block] Blogging Prompt' ], + slack_id: 'C03NLNTPZ2T', + board_id: 'https://github.com/orgs/Automattic/projects/448', + }, + 'Earn Features': { + team: 'Gold', + labels: [ 'Earn', '[Block] Paid Content', '[Block] Payments', '[Feature] Memberships' ], + slack_id: 'C01B6KEJ5GE', + board_id: 'https://github.com/orgs/Automattic/projects/718', + }, + Newsletter: { + team: 'Zap', + labels: [ '[Block] Subscriptions', '[Block] Paywall', '[Feature] Subscriptions' ], + slack_id: 'C02NQ4HMJKV', + board_id: 'https://github.com/orgs/Automattic/projects/657', + }, + Reader: { + team: 'Loop', + labels: [ '[Feature] Reader' ], + slack_id: 'C03NLNTPZ2T', + board_id: 'https://github.com/orgs/Automattic/projects/448', + }, + // Jetpack Division. + 'AI Tools': { + team: 'Agora', + labels: [ + '[Block] AI Assistant', + '[Extension] AI Content Lens', + '[Extension] AI Assistant', + '[Extension] AI Assistant Plugin', + '[AI Feature] AI Extension', + '[Package] AI', + '[JS Package] AI Client', + ], + slack_id: 'C054LN8RNVA', + board_id: 'https://github.com/orgs/Automattic/projects/667', + }, + Akismet: { + team: 'Akismet', + labels: [ '[Feature] Akismet' ], + slack_id: 'C029E4HPT', + }, + Backups: { + team: 'Backup', + labels: [ + '[Plugin] Backup', + '[Plugin] VaultPress', + '[Feature] Backup & Scan', + '[Package] Backup', + '[Package] Transport Helper', + ], + slack_id: 'CS8UYNPEE', + board_id: 'https://github.com/orgs/Automattic/projects/766', + }, + Boost: { + team: 'Heart of Gold', + labels: [ '[Plugin] Boost' ], + slack_id: 'C016BBAFHHS', + board_id: 'https://github.com/orgs/Automattic/projects/548', + }, + 'Blocks infrastructure': { + team: 'Vulcan', + labels: [ '[Package] Blocks', '[Focus] FSE', '[Focus] Blocks' ], + slack_id: 'CBG1CP4EN', + board_id: 'https://github.com/orgs/Automattic/projects/778', + }, + Connection: { + team: 'Vulcan', + labels: [ '[Package] Connection', '[Package] Identity Crisis', '[Package] Sync' ], + slack_id: 'CBG1CP4EN', + board_id: 'https://github.com/orgs/Automattic/projects/778', + }, + CRM: { + team: 'Avengers', + labels: [ '[Plugin] CRM' ], + slack_id: 'CTXBP902X', + board_id: 'https://github.com/orgs/Automattic/projects/524', + }, + 'Monorepo tooling': { + team: 'Jetpack Garage', + labels: [ '[Tools] Development CLI', 'Actions' ], + slack_id: 'CBG1CP4EN', + board_id: 'https://github.com/orgs/Automattic/projects/599', + }, + 'My Jetpack': { + team: 'Agora', + labels: [ '[Package] My Jetpack' ], + slack_id: 'C02TQF5VAJD', + }, + Protect: { + team: 'Scan', + labels: [ '[Plugin] Protect', '[Feature] Protect', '[Package] WAF' ], + slack_id: 'C029WFNV69M', + board_id: 767, + }, + 'React Dashboard': { + team: 'Vulcan', + labels: [ 'Admin Page' ], + slack_id: 'CBG1CP4EN', + board_id: 'https://github.com/orgs/Automattic/projects/778', + }, + Search: { + team: 'Red', + labels: [ '[Plugin] Search', '[Package] Search', 'Instant Search', '[Feature] Search' ], + slack_id: 'C02ME06LF', + board_id: 'https://github.com/orgs/Automattic/projects/408', + }, + 'Social tools': { + team: 'Reach', + labels: [ + '[Plugin] Social', + '[Extension] Publicize', + '[JS Package] Publicize Components', + '[Package] Publicize', + '[Feature] Publicize', + ], + slack_id: 'C02JJ910CNL', + board_id: 'https://github.com/orgs/Automattic/projects/742', + }, + Stats: { + team: 'Red', + labels: [ + '[Feature] Stats Data', + '[Package] Stats Data', + 'Stats', + 'Odyssey Stats', + 'Odyssey Stats Widget', + '[Stats] Subscribers', + ], + slack_id: 'C0438NHCLSY', + board_id: 'https://github.com/orgs/Automattic/projects/484', + }, + 'Super Cache': { + team: 'Heart of Gold', + labels: [ '[Plugin] Super Cache' ], + slack_id: 'C016BBAFHHS', + board_id: 'https://github.com/orgs/Automattic/projects/548', + }, + VideoPress: { + team: 'Agora', + labels: [ '[Package] VideoPress', '[Feature] VideoPress', '[Plugin] VideoPress' ], + slack_id: 'C02TQF5VAJD', + board_id: 'https://github.com/orgs/Automattic/projects/460', + }, + // Let this be the last item. It will act as a catch-all for any issues that haven't been matched until now. + 'Jetpack plugin': { + team: 'Jetpack', + labels: [ '[Plugin] Jetpack' ], + slack_id: 'CDLH4C1UZ', + }, +}; diff --git a/projects/github-actions/repo-gardening/src/tasks/update-board/index.js b/projects/github-actions/repo-gardening/src/tasks/update-board/index.js index a228ab69b58c9..09a1606431d30 100644 --- a/projects/github-actions/repo-gardening/src/tasks/update-board/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/update-board/index.js @@ -1,32 +1,14 @@ const { getInput, setFailed } = require( '@actions/core' ); const { getOctokit } = require( '@actions/github' ); const debug = require( '../../utils/debug' ); -const getLabels = require( '../../utils/get-labels' ); +const getLabels = require( '../../utils/labels/get-labels' ); +const hasPriorityLabels = require( '../../utils/labels/has-priority-labels' ); +const isBug = require( '../../utils/labels/is-bug' ); +const notifyImportantIssues = require( '../../utils/slack/notify-important-issues' ); +const { automatticAssignments } = require( './automattic-label-team-assignments' ); /* global GitHub, WebhookPayloadIssue */ -/** - * Check for Priority labels on an issue. - * It could be existing labels, - * or it could be that it's being added as part of the event that triggers this action. - * - * @param {GitHub} octokit - Initialized Octokit REST client. - * @param {string} owner - Repository owner. - * @param {string} repo - Repository name. - * @param {string} number - Issue number. - * @param {string} action - Action that triggered the event ('opened', 'reopened', 'labeled'). - * @param {object} eventLabel - Label that was added to the issue. - * @returns {Promise} Promise resolving to an array of Priority labels. - */ -async function hasPriorityLabels( octokit, owner, repo, number, action, eventLabel ) { - const labels = await getLabels( octokit, owner, repo, number ); - if ( 'labeled' === action && eventLabel.name && eventLabel.name.match( /^\[Pri\].*$/ ) ) { - labels.push( eventLabel.name ); - } - - return labels.filter( label => label.match( /^\[Pri\].*$/ ) && label !== '[Pri] TBD' ); -} - /** * Check if an issue has a "Triaged" label. * It could be an existing label, @@ -75,32 +57,6 @@ async function needsThirdPartyFix( octokit, owner, repo, number, action, eventLa return labels.some( label => label.match( /^\[Status\] Needs (3rd Party|Core) Fix$/ ) ); } -/** - * Ensure the issue is a bug, by looking for a "[Type] Bug" label. - * It could be an existing label, - * or it could be that it's being added as part of the event that triggers this action. - * - * @param {GitHub} octokit - Initialized Octokit REST client. - * @param {string} owner - Repository owner. - * @param {string} repo - Repository name. - * @param {string} number - Issue number. - * @param {string} action - Action that triggered the event ('opened', 'reopened', 'labeled'). - * @param {object} eventLabel - Label that was added to the issue. - * @returns {Promise} Promise resolving to boolean. - */ -async function isBug( octokit, owner, repo, number, action, eventLabel ) { - // If the issue has a "[Type] Bug" label, it's a bug. - const labels = await getLabels( octokit, owner, repo, number ); - if ( labels.includes( '[Type] Bug' ) ) { - return true; - } - - // Next, check if the current event was a [Type] Bug label being added. - if ( 'labeled' === action && eventLabel.name && '[Type] Bug' === eventLabel.name ) { - return true; - } -} - /** * Get Information about a project board. * @@ -183,6 +139,14 @@ async function getProjectDetails( octokit, projectBoardLink ) { projectInfo.status = statusField; // Info about our Status column (id as well as possible values). } + // Extract the ID of the Team field. + const teamField = projectDetails[ projectInfo.ownerType ]?.projectV2.fields.nodes.find( + field => field.name === 'Team' + ); + if ( teamField ) { + projectInfo.team = teamField; // Info about our Team column (id as well as possible values). + } + return projectInfo; } @@ -322,7 +286,7 @@ async function setPriorityField( octokit, projectInfo, projectItemId, priorityTe const newProjectItemId = projectNewItemDetails.set_priority.projectV2Item.id; if ( ! newProjectItemId ) { - debug( `update-board: Failed to set the "${ priorityText }" status for this project item.` ); + debug( `update-board: Failed to set the "${ priorityText }" priority for this project item.` ); return ''; } @@ -393,6 +357,206 @@ async function setStatusField( octokit, projectInfo, projectItemId, statusText ) return newProjectItemId; // New Project item ID (what we just edited). String. } +/** + * Update the "Team" field in our project board. + * + * @param {GitHub} octokit - Initialized Octokit REST client. + * @param {object} projectInfo - Info about our project board. + * @param {string} projectItemId - The ID of the project item. + * @param {string} team - Team that should be assigned to our issue (must match an existing column in the project board). + * @returns {Promise} - The new project item id. + */ +async function setTeamField( octokit, projectInfo, projectItemId, team ) { + const { + projectNodeId, // Project board node ID. + team: { + id: teamFieldID, // ID of the status field. + options, + }, + } = projectInfo; + + // Find the ID of the team option that matches our issue team. + const teamOptionId = options.find( option => option.name === team )?.id; + if ( ! teamOptionId ) { + debug( + `update-board: Team "${ team }" does not exist as a column option in the project board.` + ); + return ''; + } + + const projectNewItemDetails = await octokit.graphql( + `mutation ( $input: UpdateProjectV2ItemFieldValueInput! ) { + set_team: updateProjectV2ItemFieldValue( input: $input ) { + projectV2Item { + id + } + } + }`, + { + input: { + projectId: projectNodeId, + itemId: projectItemId, + fieldId: teamFieldID, + value: { + singleSelectOptionId: teamOptionId, + }, + }, + } + ); + + const newProjectItemId = projectNewItemDetails.set_team.projectV2Item.id; + if ( ! newProjectItemId ) { + debug( `update-board: Failed to set the "${ team }" team for this project item.` ); + return ''; + } + + debug( `update-board: Project item ${ newProjectItemId } was assigned to the "${ team }" team.` ); + + return newProjectItemId; // New Project item ID (what we just edited). String. +} + +/** + * Load a mapping of teams <> labels from a file. + * + * @param {string} ownerLogin - Repository owner login. + * + * @returns {Promise} - Mapping of teams <> labels. + */ +async function loadTeamAssignments( ownerLogin ) { + // If we're in an Automattic repo, we can use the team assignments file that ships with this action. + if ( 'automattic' === ownerLogin ) { + return automatticAssignments; + } + + const teamAssignmentsString = getInput( 'labels_team_assignments' ); + if ( ! teamAssignmentsString ) { + debug( + `update-board: No mapping of teams <> labels provided. Cannot automatically assign an issue to a specific team on the board. Aborting.` + ); + return {}; + } + + const teamAssignments = JSON.parse( teamAssignmentsString ); + // Check if it is a valid object and includes information about teams and labels. + if ( + ! teamAssignments || + ! Object.keys( teamAssignments ).length || + ! Object.values( teamAssignments ).some( assignment => assignment.team ) || + ! Object.values( teamAssignments ).some( assignment => assignment.labels ) + ) { + debug( + `update-board: Invalid mapping of teams <> labels provided. Cannot automatically assign an issue to a specific team on the board. Aborting.` + ); + return {}; + } + + return teamAssignments; +} + +/** + * Check if an issue has a label that matches a team. + * If so, assign the issue to that team on the project board. + * If not, do nothing. + * It could be an existing label, + * or it could be that it's being added as part of the event that triggers this action. + * + * @param {GitHub} octokit - Initialized Octokit REST client. + * @param {object} payload - Issue event payload. + * @param {object} projectInfo - Info about our project board. + * @param {string} projectItemId - The ID of the project item. + * @param {Array} priorityLabels - Array of priority labels. + * @returns {Promise} - The new project item id. + */ +async function assignTeam( octokit, payload, projectInfo, projectItemId, priorityLabels ) { + const { + action, + issue: { number, node_id }, + label = {}, + repository: { owner, name }, + } = payload; + const ownerLogin = owner.login; + + const teamAssignments = await loadTeamAssignments( ownerLogin ); + if ( ! teamAssignments ) { + debug( + `update-board: No mapping of teams <> labels provided. Cannot automatically assign an issue to a specific team on the board. Aborting.` + ); + return projectItemId; + } + + // Get the list of labels associated with this issue. + const labels = await getLabels( octokit, ownerLogin, name, number ); + if ( 'labeled' === action && label.name ) { + labels.push( label.name ); + } + + // Check if any of the labels on this issue match a team. + // Loop through all the mappings in team assignments, + // and find the first one that includes a label that matches one present in the issue. + const [ featureName, { team, slack_id, board_id } = {} ] = + Object.entries( teamAssignments ).find( ( [ , assignment ] ) => + labels.some( mappedLabel => assignment.labels.includes( mappedLabel ) ) + ) || []; + + if ( ! team ) { + debug( + `update-board: Issue #${ number } does not have a label that matches a team. Aborting.` + ); + return projectItemId; + } + + // Set the status field for this project item. + debug( + `update-board: Assigning the "${ team }" team for this project item, issue #${ number }.` + ); + projectItemId = await setTeamField( octokit, projectInfo, projectItemId, team ); + + // Does the team want to be notified in Slack about high/blocker priority issues? + if ( slack_id && priorityLabels.length > 0 ) { + debug( + `update-board: Issue #${ number } has the following priority labels: ${ priorityLabels.join( + ', ' + ) }. The ${ team } team is interested in getting Slack updates for important issues. Let’s notify them.` + ); + await notifyImportantIssues( octokit, payload, slack_id ); + } + + // Does the team have a Project board where they track work for this feature? We can add the issue to that board. + if ( board_id ) { + debug( + `update-board: Issue #${ number } is associated with the "${ featureName }" feature, and the ${ team } team has a dedicated project board for this feature. Let’s add the issue to that board.` + ); + + // Get details about our project board, to use in our requests. + const featureProjectInfo = await getProjectDetails( octokit, board_id ); + if ( Object.keys( featureProjectInfo ).length === 0 || ! featureProjectInfo.projectNodeId ) { + setFailed( + `update-board: we cannot fetch info about the project board associated to the "${ featureName }" feature. Aborting task.` + ); + return projectItemId; + } + + // Check if the issue is already on the project board. If so, return its ID on the board. + let featureIssueItemId = await getIssueProjectItemId( + octokit, + featureProjectInfo, + name, + number + ); + if ( ! featureIssueItemId ) { + debug( `update-board: Issue #${ number } is not on our project board. Let’s add it.` ); + + featureIssueItemId = await addIssueToBoard( octokit, featureProjectInfo, node_id ); + if ( ! featureIssueItemId ) { + debug( `update-board: Failed to add issue to project board. Aborting.` ); + return projectItemId; + } + } + } + + return projectItemId; +} + /** * Automatically update specific columns in our common GitHub project board, * to match labels applied to issues. @@ -466,7 +630,12 @@ async function updateBoard( payload, octokit ) { debug( `update-board: Setting the "Needs Triage" status for this project item, issue #${ number }.` ); - await setStatusField( projectOctokit, projectInfo, projectItemId, 'Needs Triage' ); + projectItemId = await setStatusField( + projectOctokit, + projectInfo, + projectItemId, + 'Needs Triage' + ); } // Check if priority needs to be updated for that issue. @@ -498,7 +667,12 @@ async function updateBoard( payload, octokit ) { debug( `update-board: Setting the "${ priorityText }" priority for this project item, issue #${ number }.` ); - await setPriorityField( projectOctokit, projectInfo, projectItemId, priorityText ); + projectItemId = await setPriorityField( + projectOctokit, + projectInfo, + projectItemId, + priorityText + ); } // Check if the issue has a "Triaged" label. @@ -533,5 +707,15 @@ async function updateBoard( payload, octokit ) { ); await setStatusField( projectOctokit, projectInfo, projectItemId, 'Triaged' ); } + + // Try to assign the issue to a specific team, if we have a mapping of teams <> labels and a matching label on the issue. + // When assigning, we can also do more to warn the team about the issue, if we have additional info (Slack, project board). + projectItemId = await assignTeam( + projectOctokit, + payload, + projectInfo, + projectItemId, + priorityLabels + ); } module.exports = updateBoard; diff --git a/projects/github-actions/repo-gardening/src/tasks/update-board/readme.md b/projects/github-actions/repo-gardening/src/tasks/update-board/readme.md index 01128f1b6b8c0..a736d435a88cb 100644 --- a/projects/github-actions/repo-gardening/src/tasks/update-board/readme.md +++ b/projects/github-actions/repo-gardening/src/tasks/update-board/readme.md @@ -2,12 +2,40 @@ This task is triggered every time labels are updated on an issue. -For now, we have 3 automations in place, both of which only triggered if an issue is already on our Project board. +For now, we have 6 automations in place: 1. If an issue is classified as a bug (it has a "[Type] bug" label), we'll add it to our project board, if it's not already there. When it gets added to the board, it should also receive the "Needs Triage" status. 2. Look for updates to the "[Pri]" labels. We'll want to automatically update the Priority field for that issue in the board, to match the label used in the issue. 3. Look for the "Triaged" label. If it has been added to an issue, let's update the status to "Triaged" in the board. +4. Look for a mapping of labels <> team provided with the workflow. If we have that, we'll look at all the labels provided in the issue, and if any of them match a label in the mapping, we'll move the issue to the corresponding team column in the board. +5. If a team has specified a custom Slack Channel ID alongside their team <> label mapping, we'll send a Slack message to that channel when an issue is moved to that team's column in the board, and if that issue is a bug with a high or blocker priority. +6. If a team has specified a custom GitHub Project Board URL alongside their team <> label mapping, we'll add the issue to that team's column in the board, when the issue is labeled with a specific label in the mapping. That will allow teams that use their own boards for triage of their work or triage of work on specific features to have those issues added to their board automatically, so they get warned before to even have to look at the general project board. ## Rationale * Ensuring our project board is as up to date as possible ensures that folks on each team can prioritize their work appropriately. + +## Usage + +- Set the `task: updateBoard` task as part of the workflow. +- Pass a custom list of label mappings as a JSON object, using `labels_team_assignments`. When specifying a new mapping, you must provide a unique feature name as key, and then a `team` value matching a column in your GitHub project board, as well as a `labels` array matching existing labels in use in your repo. No wild cards or regular expressions are supported for those arrays. You can also optionally pass a `slack_id`, matching a Slack channel ID where that team would like to be notified, as well as `board_id`, matching a GitHub project board URL where that team would like to have issues added automatically. +- **Note**: if you work in a repository in the Automattic organization, you do not need to pass a custom list. Instead, add your mappings to the existing `automatticAssignments` object in the `updateBoard` task. + +Example: +```yml + ... + with: + tasks: 'updateBoard' + labels_team_assignments: | + { + "AI Tools": { + "team": "Korvax", + "labels": [ + "[Feature] AI Tools", + "[Block] A Block Name" + ], + "slack_id": "CN2FSK7L4", + "board_id": "https://github.com/users/yourname/projects/3" + } + } +``` diff --git a/projects/github-actions/repo-gardening/src/utils/get-plugin-names.js b/projects/github-actions/repo-gardening/src/utils/get-plugin-names.js index 788ac699653a7..ad1f1926cd104 100644 --- a/projects/github-actions/repo-gardening/src/utils/get-plugin-names.js +++ b/projects/github-actions/repo-gardening/src/utils/get-plugin-names.js @@ -1,6 +1,6 @@ /* global GitHub */ -const getLabels = require( './get-labels' ); +const getLabels = require( './labels/get-labels' ); /** * Get the name of the plugin concerned by this PR. diff --git a/projects/github-actions/repo-gardening/src/utils/get-labels.js b/projects/github-actions/repo-gardening/src/utils/labels/get-labels.js similarity index 90% rename from projects/github-actions/repo-gardening/src/utils/get-labels.js rename to projects/github-actions/repo-gardening/src/utils/labels/get-labels.js index 407f5e7687311..020e7c662f491 100644 --- a/projects/github-actions/repo-gardening/src/utils/get-labels.js +++ b/projects/github-actions/repo-gardening/src/utils/labels/get-labels.js @@ -1,5 +1,5 @@ /* global GitHub */ -const debug = require( './debug' ); +const debug = require( '../debug' ); // Cache for getLabels. const cache = {}; @@ -17,7 +17,7 @@ async function getLabels( octokit, owner, repo, number ) { const labelList = []; const cacheKey = `${ owner }/${ repo } #${ number }`; if ( cache[ cacheKey ] ) { - debug( `get-labels: Returning list of lables on ${ cacheKey } from cache.` ); + debug( `get-labels: Returning list of labels on ${ cacheKey } from cache.` ); return cache[ cacheKey ]; } diff --git a/projects/github-actions/repo-gardening/src/utils/labels/has-escalated-label.js b/projects/github-actions/repo-gardening/src/utils/labels/has-escalated-label.js new file mode 100644 index 0000000000000..1790442144dcd --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/labels/has-escalated-label.js @@ -0,0 +1,38 @@ +const getLabels = require( './get-labels' ); + +/* global GitHub */ + +/** + * Check for a "[Status] Priority Review Triggered" label showing that it was already escalated. + * It could be an existing label, + * or it could be that it's being added as part of the event that triggers this action. + * + * @param {GitHub} octokit - Initialized Octokit REST client. + * @param {string} owner - Repository owner. + * @param {string} repo - Repository name. + * @param {string} number - Issue number. + * @param {string} action - Action that triggered the event ('opened', 'reopened', 'labeled'). + * @param {object} eventLabel - Label that was added to the issue. + * @returns {Promise} Promise resolving to boolean. + */ +async function hasEscalatedLabel( octokit, owner, repo, number, action, eventLabel ) { + // Check for an exisiting label first. + const labels = await getLabels( octokit, owner, repo, number ); + if ( + labels.includes( '[Status] Priority Review Triggered' ) || + labels.includes( '[Status] Escalated to Kitkat' ) + ) { + return true; + } + + // If the issue is being labeled, check if the label is "[Status] Priority Review Triggered". + if ( + 'labeled' === action && + eventLabel.name && + eventLabel.name.match( /^\[Status\] Priority Review Triggered.*$/ ) + ) { + return true; + } +} + +module.exports = hasEscalatedLabel; diff --git a/projects/github-actions/repo-gardening/src/utils/labels/has-priority-labels.js b/projects/github-actions/repo-gardening/src/utils/labels/has-priority-labels.js new file mode 100644 index 0000000000000..bb1d08a3af436 --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/labels/has-priority-labels.js @@ -0,0 +1,27 @@ +const getLabels = require( './get-labels' ); + +/* global GitHub */ + +/** + * Check for Priority labels on an issue. + * It could be existing labels, + * or it could be that it's being added as part of the event that triggers this action. + * + * @param {GitHub} octokit - Initialized Octokit REST client. + * @param {string} owner - Repository owner. + * @param {string} repo - Repository name. + * @param {string} number - Issue number. + * @param {string} action - Action that triggered the event ('opened', 'reopened', 'labeled'). + * @param {object} eventLabel - Label that was added to the issue. + * @returns {Promise} Promise resolving to an array of Priority labels. + */ +async function hasPriorityLabels( octokit, owner, repo, number, action, eventLabel ) { + const labels = await getLabels( octokit, owner, repo, number ); + if ( 'labeled' === action && eventLabel.name && eventLabel.name.match( /^\[Pri\].*$/ ) ) { + labels.push( eventLabel.name ); + } + + return labels.filter( label => label.match( /^\[Pri\].*$/ ) && label !== '[Pri] TBD' ); +} + +module.exports = hasPriorityLabels; diff --git a/projects/github-actions/repo-gardening/src/utils/labels/is-bug.js b/projects/github-actions/repo-gardening/src/utils/labels/is-bug.js new file mode 100644 index 0000000000000..ea92292d9f2e9 --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/labels/is-bug.js @@ -0,0 +1,31 @@ +const getLabels = require( './get-labels' ); + +/* global GitHub */ + +/** + * Ensure the issue is a bug, by looking for a "[Type] Bug" label. + * It could be an existing label, + * or it could be that it's being added as part of the event that triggers this action. + * + * @param {GitHub} octokit - Initialized Octokit REST client. + * @param {string} owner - Repository owner. + * @param {string} repo - Repository name. + * @param {string} number - Issue number. + * @param {string} action - Action that triggered the event ('opened', 'reopened', 'labeled'). + * @param {object} eventLabel - Label that was added to the issue. + * @returns {Promise} Promise resolving to boolean. + */ +async function isBug( octokit, owner, repo, number, action, eventLabel ) { + // If the issue has a "[Type] Bug" label, it's a bug. + const labels = await getLabels( octokit, owner, repo, number ); + if ( labels.includes( '[Type] Bug' ) ) { + return true; + } + + // Next, check if the current event was a [Type] Bug label being added. + if ( 'labeled' === action && eventLabel.name && '[Type] Bug' === eventLabel.name ) { + return true; + } +} + +module.exports = isBug; diff --git a/projects/github-actions/repo-gardening/src/utils/parse-content/find-platforms.js b/projects/github-actions/repo-gardening/src/utils/parse-content/find-platforms.js new file mode 100644 index 0000000000000..fcfa0062f949a --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/parse-content/find-platforms.js @@ -0,0 +1,23 @@ +const debug = require( '../debug' ); +/** + * Find platform info, based off issue contents. + * + * @param {string} body - The issue content. + * @returns {Array} Platforms impacted by issue. + */ +function findPlatforms( body ) { + const regex = /###\sPlatform\s\(Simple\sand\/or Atomic\)\n\n([a-zA-Z ,-]*)\n\n/gm; + + const match = regex.exec( body ); + if ( match ) { + const [ , platforms ] = match; + return platforms + .split( ', ' ) + .filter( platform => platform !== 'Self-hosted' && platform.trim() !== '' ); + } + + debug( `find-platform: no platform indicators found.` ); + return []; +} + +module.exports = findPlatforms; diff --git a/projects/github-actions/repo-gardening/src/utils/parse-content/find-plugins.js b/projects/github-actions/repo-gardening/src/utils/parse-content/find-plugins.js new file mode 100644 index 0000000000000..202c6bed17eda --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/parse-content/find-plugins.js @@ -0,0 +1,22 @@ +const debug = require( '../debug' ); + +/** + * Find list of plugins impacted by issue, based off issue contents. + * + * @param {string} body - The issue content. + * @returns {Array} Plugins concerned by issue. + */ +function findPlugins( body ) { + const regex = /###\sImpacted\splugin\n\n([a-zA-Z ,]*)\n\n/gm; + + const match = regex.exec( body ); + if ( match ) { + const [ , plugins ] = match; + return plugins.split( ', ' ).filter( v => v.trim() !== '' ); + } + + debug( `find-plugins: No plugin indicators found.` ); + return []; +} + +module.exports = findPlugins; diff --git a/projects/github-actions/repo-gardening/src/utils/parse-content/find-priority.js b/projects/github-actions/repo-gardening/src/utils/parse-content/find-priority.js new file mode 100644 index 0000000000000..799fdee83de80 --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/parse-content/find-priority.js @@ -0,0 +1,38 @@ +const debug = require( '../debug' ); + +/** + * Figure out the priority of the issue, based off issue contents. + * Logic follows this priority matrix: pciE2j-oG-p2 + * + * @param {string} body - The issue content. + * @returns {string} Priority of issue. + */ +function findPriority( body ) { + // Look for priority indicators in body. + const priorityRegex = + /###\sImpact\n\n(?.*)\n\n###\sAvailable\sworkarounds\?\n\n(?.*)\n/gm; + let match; + while ( ( match = priorityRegex.exec( body ) ) ) { + const [ , impact = '', blocking = '' ] = match; + + debug( + `find-priority: Reported priority indicators for issue: "${ impact }" / "${ blocking }"` + ); + + if ( blocking === 'No and the platform is unusable' ) { + return impact === 'One' ? 'High' : 'BLOCKER'; + } else if ( blocking === 'No but the platform is still usable' ) { + return 'High'; + } else if ( blocking === 'Yes, difficult to implement' ) { + return impact === 'All' ? 'High' : 'Normal'; + } else if ( blocking !== '' && blocking !== '_No response_' ) { + return impact === 'All' || impact === 'Most (> 50%)' ? 'Normal' : 'Low'; + } + return 'TBD'; + } + + debug( `find-priority: No priority indicators found.` ); + return 'TBD'; +} + +module.exports = findPriority; diff --git a/projects/github-actions/repo-gardening/src/utils/parse-content/has-many-support-references.js b/projects/github-actions/repo-gardening/src/utils/parse-content/has-many-support-references.js new file mode 100644 index 0000000000000..7839fa5368ea0 --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/parse-content/has-many-support-references.js @@ -0,0 +1,32 @@ +const { getInput } = require( '@actions/core' ); + +/** + * Check if the issue has a comment with a list of support references, + * and at least x support references listed there. + * (x is specified with reply_to_customers_threshold input, default to 10). + * We only count the number of unanswered support references, since they're the ones we'll need to contact. + * + * @param {Array} issueComments - Array of all comments on that issue. + * @returns {Promise} Promise resolving to boolean. + */ +async function hasManySupportReferences( issueComments ) { + const referencesThreshhold = getInput( 'reply_to_customers_threshold' ); + + let isWidelySpreadIssue = false; + issueComments.map( comment => { + if ( + comment.user.login === 'github-actions[bot]' && + comment.body.includes( '**Support References**' ) + ) { + // Count the number of to-do items in the comment. + const countReferences = comment.body.split( '- [ ] ' ).length - 1; + if ( countReferences >= parseInt( referencesThreshhold ) ) { + isWidelySpreadIssue = true; + } + } + } ); + + return isWidelySpreadIssue; +} + +module.exports = hasManySupportReferences; diff --git a/projects/github-actions/repo-gardening/src/utils/slack/format-slack-message.js b/projects/github-actions/repo-gardening/src/utils/slack/format-slack-message.js new file mode 100644 index 0000000000000..eceef0c8f57b6 --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/slack/format-slack-message.js @@ -0,0 +1,44 @@ +/* global WebhookPayloadIssue */ + +/** + * Build an object containing the slack message and its formatting to send to Slack. + * This is a basic message. For more complex messages, you can build your own object and pass it to the sendSlackMessage function. + * + * @param {WebhookPayloadIssue} payload - Issue event payload. + * @param {string} channel - Slack channel ID. + * @param {string} message - Basic message (without the formatting). + * @returns {object} Object containing the slack message and its formatting. + */ +function formatSlackMessage( payload, channel, message ) { + const { issue } = payload; + const { html_url, title } = issue; + + return { + channel, + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: message, + }, + }, + { + type: 'divider', + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `<${ html_url }|${ title }>`, + }, + }, + ], + text: `${ message } -- <${ html_url }|${ title }>`, // Fallback text for display in notifications. + mrkdwn: true, // Formatting of the fallback text. + unfurl_links: false, + unfurl_media: false, + }; +} + +module.exports = formatSlackMessage; diff --git a/projects/github-actions/repo-gardening/src/utils/slack/notify-important-issues.js b/projects/github-actions/repo-gardening/src/utils/slack/notify-important-issues.js new file mode 100644 index 0000000000000..424749ffc2a63 --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/slack/notify-important-issues.js @@ -0,0 +1,75 @@ +const debug = require( '../debug' ); +const hasEscalatedLabel = require( '../labels/has-escalated-label' ); +const hasPriorityLabels = require( '../labels/has-priority-labels' ); +const isBug = require( '../labels/is-bug' ); +const findPriority = require( '../parse-content/find-priority' ); +const formatSlackMessage = require( './format-slack-message' ); +const sendSlackMessage = require( './send-slack-message' ); + +/* global GitHub, WebhookPayloadIssue */ + +/** + * Send a Slack Notification if the issue is important. + * + * We define an important issue when meeting all of the following criteria: + * - A bug (includes a "[Type] Bug" label, or a "[Type] Bug" label is added to the issue right now) + * - The issue is still opened + * - The issue is not escalated yet (no "[Status] Priority Review Triggered" label) + * - The issue is either a high priority or a blocker (inferred from the existing labels or from the issue body) + * - The issue is not already set to another priority label (no "[Pri] High", "[Pri] BLOCKER", or "[Pri] TBD" label) + * + * @param {GitHub} octokit - Initialized Octokit REST client. + * @param {WebhookPayloadIssue} payload - Issue event payload. + * @param {string} channel - Slack channel ID to send the message to. + */ +async function notifyImportantIssues( octokit, payload, channel ) { + const { action, issue, label = {}, repository } = payload; + const { number, body, state } = issue; + const { owner, name } = repository; + const ownerLogin = owner.login; + + const isBugIssue = await isBug( octokit, ownerLogin, name, number, action, label ); + const isEscalated = await hasEscalatedLabel( octokit, ownerLogin, name, number, action, label ); + const priorityLabels = await hasPriorityLabels( + octokit, + ownerLogin, + name, + number, + action, + label + ); + const priority = findPriority( body ); + + const highPriorityIssue = priority === 'High' || priorityLabels.includes( '[Pri] High' ); + const blockerIssue = priority === 'BLOCKER' || priorityLabels.includes( '[Pri] BLOCKER' ); + + const hasOtherPriorityLabels = priorityLabels.some( priLabel => + /^\[Pri\] (?!High|BLOCKER|TBD)/.test( priLabel ) + ); + + if ( + isBugIssue && + state === 'open' && + ! isEscalated && + ( highPriorityIssue || blockerIssue ) && + ! hasOtherPriorityLabels + ) { + const message = `New ${ + highPriorityIssue ? 'High-priority' : 'Blocker' + } bug! Please check the priority.`; + const slackMessageFormat = formatSlackMessage( payload, channel, message ); + await sendSlackMessage( message, channel, payload, slackMessageFormat ); + + debug( + `notify-important-issues: Adding a label to issue #${ number } to show that the triage team was warned.` + ); + await octokit.rest.issues.addLabels( { + owner: ownerLogin, + repo: name, + issue_number: number, + labels: [ '[Status] Priority Review Triggered' ], + } ); + } +} + +module.exports = notifyImportantIssues; diff --git a/projects/github-actions/repo-gardening/src/utils/send-slack-message.js b/projects/github-actions/repo-gardening/src/utils/slack/send-slack-message.js similarity index 88% rename from projects/github-actions/repo-gardening/src/utils/send-slack-message.js rename to projects/github-actions/repo-gardening/src/utils/slack/send-slack-message.js index 0ab7978f76f89..c5e7f14527709 100644 --- a/projects/github-actions/repo-gardening/src/utils/send-slack-message.js +++ b/projects/github-actions/repo-gardening/src/utils/slack/send-slack-message.js @@ -1,3 +1,4 @@ +const { getInput, setFailed } = require( '@actions/core' ); const fetch = require( 'node-fetch' ); /* global WebhookPayloadPullRequest, WebhookPayloadIssue */ @@ -7,12 +8,17 @@ const fetch = require( 'node-fetch' ); * * @param {string} message - Message to post to Slack * @param {string} channel - Slack channel ID. - * @param {string} token - Slack token. * @param {WebhookPayloadPullRequest|WebhookPayloadIssue} payload - Pull request event payload. * @param {object} customMessageFormat - Custom message formatting. If defined, takes over from message completely. * @returns {Promise} Promise resolving to a boolean, whether message was successfully posted or not. */ -async function sendSlackMessage( message, channel, token, payload, customMessageFormat = {} ) { +async function sendSlackMessage( message, channel, payload, customMessageFormat = {} ) { + const token = getInput( 'slack_token' ); + if ( ! token ) { + setFailed( 'triage-issues: Input slack_token is required but missing. Aborting.' ); + return; + } + let slackMessage = ''; // If we have a custom message format, use it. diff --git a/projects/js-packages/babel-plugin-replace-textdomain/CHANGELOG.md b/projects/js-packages/babel-plugin-replace-textdomain/CHANGELOG.md index 940fefbae0e87..d24478ee1bee8 100644 --- a/projects/js-packages/babel-plugin-replace-textdomain/CHANGELOG.md +++ b/projects/js-packages/babel-plugin-replace-textdomain/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.33] - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## [1.0.32] - 2023-12-03 ### Changed - Updated package dependencies. [#34427] @@ -149,6 +153,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release. - Replace missing domains too. +[1.0.33]: https://github.com/Automattic/babel-plugin-replace-textdomain/compare/v1.0.32...v1.0.33 [1.0.32]: https://github.com/Automattic/babel-plugin-replace-textdomain/compare/v1.0.31...v1.0.32 [1.0.31]: https://github.com/Automattic/babel-plugin-replace-textdomain/compare/v1.0.30...v1.0.31 [1.0.30]: https://github.com/Automattic/babel-plugin-replace-textdomain/compare/v1.0.29...v1.0.30 diff --git a/projects/js-packages/babel-plugin-replace-textdomain/package.json b/projects/js-packages/babel-plugin-replace-textdomain/package.json index 2883b356d9b03..76bbb2cfc15e8 100644 --- a/projects/js-packages/babel-plugin-replace-textdomain/package.json +++ b/projects/js-packages/babel-plugin-replace-textdomain/package.json @@ -1,6 +1,6 @@ { "name": "@automattic/babel-plugin-replace-textdomain", - "version": "1.0.33-alpha", + "version": "1.0.33", "description": "A Babel plugin to replace the textdomain in gettext-style function calls.", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/babel-plugin-replace-textdomain/#readme", "bugs": { diff --git a/projects/js-packages/components/CHANGELOG.md b/projects/js-packages/components/CHANGELOG.md index 848ab2655e657..6675bc3e1b3bd 100644 --- a/projects/js-packages/components/CHANGELOG.md +++ b/projects/js-packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ### This is a list detailing changes for the Jetpack RNA Components package releases. +## [0.45.5] - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## [0.45.4] - 2023-12-03 ### Changed - Updated package dependencies. [#34411] [#34427] @@ -890,6 +894,7 @@ ### Changed - Update node version requirement to 14.16.1 +[0.45.5]: https://github.com/Automattic/jetpack-components/compare/0.45.4...0.45.5 [0.45.4]: https://github.com/Automattic/jetpack-components/compare/0.45.3...0.45.4 [0.45.3]: https://github.com/Automattic/jetpack-components/compare/0.45.2...0.45.3 [0.45.2]: https://github.com/Automattic/jetpack-components/compare/0.45.1...0.45.2 diff --git a/projects/js-packages/components/package.json b/projects/js-packages/components/package.json index 62c4e9f44cc49..59efe83c4e127 100644 --- a/projects/js-packages/components/package.json +++ b/projects/js-packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@automattic/jetpack-components", - "version": "0.45.5-alpha", + "version": "0.45.5", "description": "Jetpack Components Package", "author": "Automattic", "license": "GPL-2.0-or-later", diff --git a/projects/js-packages/connection/CHANGELOG.md b/projects/js-packages/connection/CHANGELOG.md index 7a1c8a7ab3fd7..9c1e45e268bac 100644 --- a/projects/js-packages/connection/CHANGELOG.md +++ b/projects/js-packages/connection/CHANGELOG.md @@ -2,6 +2,10 @@ ### This is a list detailing changes for the Jetpack RNA Connection Component releases. +## [0.30.10] - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## [0.30.9] - 2023-12-03 ### Changed - Updated package dependencies. [#34411] [#34427] @@ -668,6 +672,7 @@ - `Main` and `ConnectUser` components added. - `JetpackRestApiClient` API client added. +[0.30.10]: https://github.com/Automattic/jetpack-connection-js/compare/v0.30.9...v0.30.10 [0.30.9]: https://github.com/Automattic/jetpack-connection-js/compare/v0.30.8...v0.30.9 [0.30.8]: https://github.com/Automattic/jetpack-connection-js/compare/v0.30.7...v0.30.8 [0.30.7]: https://github.com/Automattic/jetpack-connection-js/compare/v0.30.6...v0.30.7 diff --git a/projects/js-packages/connection/changelog/renovate-babel-monorepo b/projects/js-packages/connection/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/js-packages/connection/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/js-packages/connection/package.json b/projects/js-packages/connection/package.json index 203fd245eb349..60f6f934e5352 100644 --- a/projects/js-packages/connection/package.json +++ b/projects/js-packages/connection/package.json @@ -1,6 +1,6 @@ { "name": "@automattic/jetpack-connection", - "version": "0.30.10-alpha", + "version": "0.30.10", "description": "Jetpack Connection Component", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/connection/#readme", "bugs": { diff --git a/projects/js-packages/i18n-check-webpack-plugin/CHANGELOG.md b/projects/js-packages/i18n-check-webpack-plugin/CHANGELOG.md index ec9bad734ace7..2b7b1dfa6b8b7 100644 --- a/projects/js-packages/i18n-check-webpack-plugin/CHANGELOG.md +++ b/projects/js-packages/i18n-check-webpack-plugin/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.5] - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## [1.1.4] - 2023-12-03 ### Changed - Updated package dependencies. [#34427] @@ -190,6 +194,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Initial release. +[1.1.5]: https://github.com/Automattic/i18n-check-webpack-plugin/compare/v1.1.4...v1.1.5 [1.1.4]: https://github.com/Automattic/i18n-check-webpack-plugin/compare/v1.1.3...v1.1.4 [1.1.3]: https://github.com/Automattic/i18n-check-webpack-plugin/compare/v1.1.2...v1.1.3 [1.1.2]: https://github.com/Automattic/i18n-check-webpack-plugin/compare/v1.1.1...v1.1.2 diff --git a/projects/js-packages/i18n-check-webpack-plugin/changelog/renovate-babel-monorepo b/projects/js-packages/i18n-check-webpack-plugin/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/js-packages/i18n-check-webpack-plugin/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/js-packages/i18n-check-webpack-plugin/package.json b/projects/js-packages/i18n-check-webpack-plugin/package.json index ab7dc9fe0f5a4..f57dc3be39238 100644 --- a/projects/js-packages/i18n-check-webpack-plugin/package.json +++ b/projects/js-packages/i18n-check-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@automattic/i18n-check-webpack-plugin", - "version": "1.1.5-alpha", + "version": "1.1.5", "description": "A Webpack plugin to check that WordPress i18n hasn't been mangled by Webpack optimizations.", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/i18n-check-webpack-plugin/#readme", "bugs": { diff --git a/projects/js-packages/idc/CHANGELOG.md b/projects/js-packages/idc/CHANGELOG.md index 0ddc74a6788b7..5ddba4aa7753c 100644 --- a/projects/js-packages/idc/CHANGELOG.md +++ b/projects/js-packages/idc/CHANGELOG.md @@ -2,6 +2,10 @@ ### This is a list detailing changes for the Jetpack RNA IDC package releases. +## 0.10.55 - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## 0.10.54 - 2023-12-03 ### Changed - Updated package dependencies. [#34411] diff --git a/projects/js-packages/idc/changelog/renovate-babel-monorepo b/projects/js-packages/idc/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/js-packages/idc/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/js-packages/idc/package.json b/projects/js-packages/idc/package.json index 0c212b5bde141..072558d9d70c4 100644 --- a/projects/js-packages/idc/package.json +++ b/projects/js-packages/idc/package.json @@ -1,6 +1,6 @@ { "name": "@automattic/jetpack-idc", - "version": "0.10.55-alpha", + "version": "0.10.55", "description": "Jetpack Connection Component", "author": "Automattic", "license": "GPL-2.0-or-later", diff --git a/projects/js-packages/licensing/CHANGELOG.md b/projects/js-packages/licensing/CHANGELOG.md index 7dc4649f72df1..db32e72d3e317 100644 --- a/projects/js-packages/licensing/CHANGELOG.md +++ b/projects/js-packages/licensing/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.11.15 - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## 0.11.14 - 2023-12-03 ### Changed - Updated package dependencies. [#34411] [#34427] diff --git a/projects/js-packages/licensing/changelog/renovate-babel-monorepo b/projects/js-packages/licensing/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/js-packages/licensing/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/js-packages/licensing/package.json b/projects/js-packages/licensing/package.json index 222bcfe33d77e..f9732bf36820d 100644 --- a/projects/js-packages/licensing/package.json +++ b/projects/js-packages/licensing/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@automattic/jetpack-licensing", - "version": "0.11.15-alpha", + "version": "0.11.15", "description": "Jetpack licensing flow", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/licensing/#readme", "bugs": { diff --git a/projects/js-packages/publicize-components/CHANGELOG.md b/projects/js-packages/publicize-components/CHANGELOG.md index 5fd20d6e998fb..1b200508e4ff7 100644 --- a/projects/js-packages/publicize-components/CHANGELOG.md +++ b/projects/js-packages/publicize-components/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.41.9] - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## [0.41.8] - 2023-12-03 ### Changed - Disabled quick share for scheduled posts. [#34354] @@ -526,6 +530,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Updated package dependencies. [#24470] +[0.41.9]: https://github.com/Automattic/jetpack-publicize-components/compare/v0.41.8...v0.41.9 [0.41.8]: https://github.com/Automattic/jetpack-publicize-components/compare/v0.41.7...v0.41.8 [0.41.7]: https://github.com/Automattic/jetpack-publicize-components/compare/v0.41.6...v0.41.7 [0.41.6]: https://github.com/Automattic/jetpack-publicize-components/compare/v0.41.5...v0.41.6 diff --git a/projects/js-packages/publicize-components/changelog/renovate-babel-monorepo b/projects/js-packages/publicize-components/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/js-packages/publicize-components/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/update-jitm-tracking-props b/projects/js-packages/react-data-sync-client/changelog/boost-react-datasync-improvements similarity index 50% rename from projects/plugins/social/changelog/update-jitm-tracking-props rename to projects/js-packages/react-data-sync-client/changelog/boost-react-datasync-improvements index 9aa70e3ec1f75..309f411b66813 100644 --- a/projects/plugins/social/changelog/update-jitm-tracking-props +++ b/projects/js-packages/react-data-sync-client/changelog/boost-react-datasync-improvements @@ -1,5 +1,5 @@ Significance: patch Type: changed -Comment: Updated composer.lock. +Comment: Changes internal interface diff --git a/projects/js-packages/react-data-sync-client/src/DataSync.ts b/projects/js-packages/react-data-sync-client/src/DataSync.ts index 227405c7e2d50..7649c0ebec7db 100644 --- a/projects/js-packages/react-data-sync-client/src/DataSync.ts +++ b/projects/js-packages/react-data-sync-client/src/DataSync.ts @@ -4,7 +4,7 @@ import type { JSONSchema, ParsedValue } from './types'; type RequestParams = string | JSONSchema; type RequestMethods = 'GET' | 'POST' | 'DELETE'; - +type GetRequestParams = Record< string, string | number | null | Array< string | number | null > >; /** * DataSync class for synchronizing data between the client and the server. * @@ -162,10 +162,18 @@ export class DataSync< Schema extends z.ZodSchema, Value extends z.infer< Schema private async request( method: RequestMethods, partialPathname: string, - params?: RequestParams, + value?: RequestParams, + params?: GetRequestParams, abortSignal?: AbortSignal ) { - const url = `${ this.wpDatasyncUrl }/${ partialPathname }`; + const url = new URL( `${ this.wpDatasyncUrl }/${ partialPathname }` ); + + if ( params ) { + Object.keys( params ).forEach( key => { + url.searchParams.append( key, params[ key ].toString() ); + } ); + } + const args: RequestInit = { method, signal: abortSignal, @@ -179,10 +187,10 @@ export class DataSync< Schema extends z.ZodSchema, Value extends z.infer< Schema }; if ( method === 'POST' ) { - args.body = JSON.stringify( { JSON: params } ); + args.body = JSON.stringify( { JSON: value } ); } - const result = await this.attemptRequest( url, args ); + const result = await this.attemptRequest( url.toString(), args ); let data; const text = await result.text(); @@ -191,7 +199,7 @@ export class DataSync< Schema extends z.ZodSchema, Value extends z.infer< Schema } catch ( e ) { // eslint-disable-next-line no-console console.error( 'Failed to parse the response\n', { url, text, result, error: e } ); - throw new ApiError( url, 'json_parse_error', 'Failed to parse the response' ); + throw new ApiError( url.toString(), 'json_parse_error', 'Failed to parse the response' ); } /** @@ -204,7 +212,7 @@ export class DataSync< Schema extends z.ZodSchema, Value extends z.infer< Schema if ( ! data || data.JSON === undefined ) { // eslint-disable-next-line no-console console.error( 'JSON response is empty.\n', { url, text, result } ); - throw new ApiError( url, 'json_empty', 'JSON response is empty' ); + throw new ApiError( url.toString(), 'json_empty', 'JSON response is empty' ); } return data.JSON; @@ -214,17 +222,18 @@ export class DataSync< Schema extends z.ZodSchema, Value extends z.infer< Schema * Method to parse the request. * @param method - The request method. * @param requestPath - The request path. - * @param params - The request parameters. + * @param value - The request parameters. * @param abortSignal - The abort signal. * @returns The parsed value. */ private async parsedRequest( method: RequestMethods, requestPath = '', - params?: Value, + value?: Value, + params: GetRequestParams = {}, abortSignal?: AbortSignal ): Promise< Value > { - const data = await this.request( method, requestPath, params, abortSignal ); + const data = await this.request( method, requestPath, value, params, abortSignal ); try { const parsed = this.schema.parse( data ); return parsed; @@ -263,20 +272,29 @@ export class DataSync< Schema extends z.ZodSchema, Value extends z.infer< Schema * to be bound to the class instance, to make it easier to pass them * around as callbacks without losing the `this` context. */ - public GET = async ( abortSignal?: AbortSignal ): Promise< Value > => { - return await this.parsedRequest( 'GET', this.endpoint, undefined, abortSignal ); - }; - - public SET = async ( params: Value, abortSignal?: AbortSignal ): Promise< Value > => { - return await this.parsedRequest( 'POST', `${ this.endpoint }/set`, params, abortSignal ); + public GET = async ( + params: GetRequestParams = {}, + abortSignal?: AbortSignal + ): Promise< Value > => { + return await this.parsedRequest( 'GET', this.endpoint, undefined, params, abortSignal ); }; - public MERGE = async ( params: Value, abortSignal?: AbortSignal ): Promise< Value > => { - return await this.parsedRequest( 'POST', `${ this.endpoint }/merge`, params, abortSignal ); + public SET = async ( + value: Value, + params: GetRequestParams = {}, + abortSignal?: AbortSignal + ): Promise< Value > => { + return await this.parsedRequest( 'POST', `${ this.endpoint }/set`, value, params, abortSignal ); }; - public DELETE = async ( abortSignal?: AbortSignal ) => { - return await this.parsedRequest( 'POST', `${ this.endpoint }/delete`, undefined, abortSignal ); + public DELETE = async ( params: GetRequestParams = {}, abortSignal?: AbortSignal ) => { + return await this.parsedRequest( + 'POST', + `${ this.endpoint }/delete`, + undefined, + params, + abortSignal + ); }; /** diff --git a/projects/js-packages/react-data-sync-client/src/DataSyncHooks.ts b/projects/js-packages/react-data-sync-client/src/DataSyncHooks.ts index afd6d67954528..a1826436075fb 100644 --- a/projects/js-packages/react-data-sync-client/src/DataSyncHooks.ts +++ b/projects/js-packages/react-data-sync-client/src/DataSyncHooks.ts @@ -14,6 +14,11 @@ import { DataSync } from './DataSync'; const queryClient = new QueryClient(); +/** + * React Query Provider for DataSync. + * This is necessary for React Query to work. + * @see https://tanstack.com/query/v5/docs/react/reference/QueryClientProvider + */ export function DataSyncProvider( props: { children: React.ReactNode } ) { return QueryClientProvider( { client: queryClient, @@ -21,28 +26,76 @@ export function DataSyncProvider( props: { children: React.ReactNode } ) { } ); } -type DataSyncFactory< T > = { - useQuery: ( config?: Omit< UseQueryOptions< T >, 'queryKey' > ) => UseQueryResult< T >; - useMutation: ( - config?: Omit< UseMutationOptions< T >, 'mutationKey' > - ) => UseMutationResult< T >; +/** + * React Query configuration type for DataSync. + */ +type DataSyncConfig< Schema extends z.ZodSchema, Value extends z.infer< Schema > > = { + query?: Omit< UseQueryOptions< Value >, 'queryKey' >; + mutation?: Omit< UseMutationOptions< Value >, 'mutationKey' >; }; +/** + * This is what `useDataSync` returns + */ +type DataSyncHook< Schema extends z.ZodSchema, Value extends z.infer< Schema > > = [ + UseQueryResult< Value >, + UseMutationResult< Value >, +]; +/** + * React Query hook for DataSync. + * @param namespace - The namespace of the endpoint. + * @param key - The key of the value that's being synced. + * @param schema - The Zod schema to validate the value against. + * @param config - React Query configuration. + * @returns A tuple of React Query hooks. + * @see https://tanstack.com/query/v5/docs/react/reference/useQuery + * @see https://tanstack.com/query/v5/docs/react/reference/useMutation + */ export function useDataSync< Schema extends z.ZodSchema, Value extends z.infer< Schema >, Key extends string, ->( namespace: string, key: Key, schema: Schema ): DataSyncFactory< Value > { +>( + namespace: string, + key: Key, + schema: Schema, + config: DataSyncConfig< Schema, Value > = {}, + params: Record< string, string | number > = {} +): DataSyncHook< Schema, Value > { const datasync = new DataSync( namespace, key, schema ); - const queryKey = [ key ]; + const queryKey = [ key, ...Object.values( params ) ]; + /** + * Defaults for `useQuery`: + * - `queryKey` is the key of the value that's being synced. + * - `queryFn` is wired up to DataSync `GET` method. + * - `initialData` gets the value from the global window object. + * + * If your property is lazy-loaded, you should populate `initialData` with a value manually. + * ```js + * const [ data ] = useDataSync( 'namespace', 'key', schema, { + * initialData: { foo: 'bar' }, + * } ); + * ``` + */ const queryConfigDefaults = { queryKey, - queryFn: ( { signal } ) => datasync.GET( signal ), + queryFn: ( { signal } ) => datasync.GET( params, signal ), initialData: datasync.getInitialValue(), }; + + /** + * Defaults for `useMutation`: + * - `mutationKey` is the key of the value that's being synced. + * - `mutationFn` is wired up to DataSync `SET` method. + * - `onMutate` is used to optimistically update the value before the request is made. + * - `onError` is used to revert the value back to the previous value if the request fails. + * - `onSettled` is used to invalidate the query after the request is made. + * + * @see https://tanstack.com/query/v5/docs/react/guides/optimistic-updates + */ const mutationConfigDefaults = { mutationKey: queryKey, - mutationFn: datasync.SET, + mutationFn: value => datasync.SET( value, params ), onMutate: async data => { const value = schema.parse( data ); @@ -53,7 +106,7 @@ export function useDataSync< // Snapshot the previous value const previousValue = queryClient.getQueryData( queryKey ); - // Optimistically update to the new value + // Optimistically update the cached state to the new value queryClient.setQueryData( queryKey, value ); // Return a context object with the snapshotted value @@ -67,8 +120,8 @@ export function useDataSync< }, }; - return { - useQuery: ( config = {} ) => useQuery( { ...queryConfigDefaults, ...config } ), - useMutation: ( config = {} ) => useMutation( { ...mutationConfigDefaults, ...config } ), - }; + return [ + useQuery( { ...queryConfigDefaults, ...config.query } ), + useMutation( { ...mutationConfigDefaults, ...config.mutation } ), + ]; } diff --git a/projects/js-packages/shared-extension-utils/CHANGELOG.md b/projects/js-packages/shared-extension-utils/CHANGELOG.md index a65b92b719542..d83fbb00bed29 100644 --- a/projects/js-packages/shared-extension-utils/CHANGELOG.md +++ b/projects/js-packages/shared-extension-utils/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.13.4] - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## [0.13.3] - 2023-12-03 ### Changed - Updated package dependencies. [#34411] [#34427] @@ -294,6 +298,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Core: prepare utility for release +[0.13.4]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.13.3...0.13.4 [0.13.3]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.13.2...0.13.3 [0.13.2]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.13.1...0.13.2 [0.13.1]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.13.0...0.13.1 diff --git a/projects/js-packages/shared-extension-utils/changelog/renovate-babel-monorepo b/projects/js-packages/shared-extension-utils/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/js-packages/shared-extension-utils/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/js-packages/shared-extension-utils/package.json b/projects/js-packages/shared-extension-utils/package.json index 614eec026b3a4..f9e44ea1a569f 100644 --- a/projects/js-packages/shared-extension-utils/package.json +++ b/projects/js-packages/shared-extension-utils/package.json @@ -1,6 +1,6 @@ { "name": "@automattic/jetpack-shared-extension-utils", - "version": "0.13.4-alpha", + "version": "0.13.4", "description": "Utility functions used by the block editor extensions", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/shared-extension-utils/#readme", "bugs": { diff --git a/projects/js-packages/webpack-config/CHANGELOG.md b/projects/js-packages/webpack-config/CHANGELOG.md index 4b6c773d3a3ae..03503952ad0e2 100644 --- a/projects/js-packages/webpack-config/CHANGELOG.md +++ b/projects/js-packages/webpack-config/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.0.4 - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## 3.0.3 - 2023-12-03 ### Changed - Updated package dependencies. [#34411] diff --git a/projects/js-packages/webpack-config/changelog/renovate-babel-monorepo b/projects/js-packages/webpack-config/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/js-packages/webpack-config/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/js-packages/webpack-config/package.json b/projects/js-packages/webpack-config/package.json index 3839e408bde01..b488f44d81622 100644 --- a/projects/js-packages/webpack-config/package.json +++ b/projects/js-packages/webpack-config/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@automattic/jetpack-webpack-config", - "version": "3.0.4-alpha", + "version": "3.0.4", "description": "Library of pieces for webpack config in Jetpack projects.", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/webpack-config/#readme", "bugs": { diff --git a/projects/js-packages/babel-plugin-replace-textdomain/changelog/renovate-babel-monorepo b/projects/packages/assets/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/js-packages/babel-plugin-replace-textdomain/changelog/renovate-babel-monorepo rename to projects/packages/assets/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/packages/assets/composer.json b/projects/packages/assets/composer.json index 008a609a188d2..80d91e3a02090 100644 --- a/projects/packages/assets/composer.json +++ b/projects/packages/assets/composer.json @@ -11,7 +11,7 @@ "brain/monkey": "2.6.1", "yoast/phpunit-polyfills": "1.1.0", "automattic/jetpack-changelogger": "@dev", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0" + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0" }, "suggest": { "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." diff --git a/projects/js-packages/components/changelog/renovate-babel-monorepo b/projects/packages/changelogger/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/js-packages/components/changelog/renovate-babel-monorepo rename to projects/packages/changelogger/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/packages/changelogger/composer.json b/projects/packages/changelogger/composer.json index 47e13ba1ac597..dfbb49a3db474 100644 --- a/projects/packages/changelogger/composer.json +++ b/projects/packages/changelogger/composer.json @@ -16,7 +16,7 @@ }, "require-dev": { "yoast/phpunit-polyfills": "1.1.0", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0" + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0" }, "autoload": { "psr-4": { diff --git a/projects/packages/changelogger/src/Application.php b/projects/packages/changelogger/src/Application.php index d60107f5c422b..367b4a733d9b7 100644 --- a/projects/packages/changelogger/src/Application.php +++ b/projects/packages/changelogger/src/Application.php @@ -18,7 +18,7 @@ */ class Application extends SymfonyApplication { - const VERSION = '4.0.4'; + const VERSION = '4.0.5-alpha'; /** * Constructor. diff --git a/projects/packages/forms/changelog/add-contact-form-onblur-validation b/projects/packages/forms/changelog/add-contact-form-onblur-validation new file mode 100644 index 0000000000000..baf134a88f185 --- /dev/null +++ b/projects/packages/forms/changelog/add-contact-form-onblur-validation @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Contact Form: revalidate fields on focus out diff --git a/projects/packages/forms/changelog/fix-contact-form-assets-cache-bust b/projects/packages/forms/changelog/fix-contact-form-assets-cache-bust new file mode 100644 index 0000000000000..789a91849f327 --- /dev/null +++ b/projects/packages/forms/changelog/fix-contact-form-assets-cache-bust @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Contact Form: specify version for accessible-form script diff --git a/projects/packages/forms/changelog/fix-contact-form-editor-checkbox b/projects/packages/forms/changelog/fix-contact-form-editor-checkbox new file mode 100644 index 0000000000000..375d530014cc5 --- /dev/null +++ b/projects/packages/forms/changelog/fix-contact-form-editor-checkbox @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Contact Form: fix checkbox field layout in editor diff --git a/projects/packages/forms/changelog/fix-contact-form-error-message-margin b/projects/packages/forms/changelog/fix-contact-form-error-message-margin new file mode 100644 index 0000000000000..cd96184e476c6 --- /dev/null +++ b/projects/packages/forms/changelog/fix-contact-form-error-message-margin @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Contact Form: add margin below global error message diff --git a/projects/packages/forms/changelog/fix-contact-form-warning-icon b/projects/packages/forms/changelog/fix-contact-form-warning-icon new file mode 100644 index 0000000000000..6bb612985f9b8 --- /dev/null +++ b/projects/packages/forms/changelog/fix-contact-form-warning-icon @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Contact Form: ensure warning icons are visible diff --git a/projects/packages/forms/changelog/refactor-contact-form-accessible-script b/projects/packages/forms/changelog/refactor-contact-form-accessible-script new file mode 100644 index 0000000000000..f32e69717ce07 --- /dev/null +++ b/projects/packages/forms/changelog/refactor-contact-form-accessible-script @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Contact Form: refactored accessible-form.js diff --git a/projects/packages/forms/changelog/update-contact-form-checkbox-radio-layout b/projects/packages/forms/changelog/update-contact-form-checkbox-radio-layout new file mode 100644 index 0000000000000..c05595feec0b0 --- /dev/null +++ b/projects/packages/forms/changelog/update-contact-form-checkbox-radio-layout @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Contact Form: align checkbox and radio button baselines diff --git a/projects/packages/forms/changelog/update-contact-form-submitting-state b/projects/packages/forms/changelog/update-contact-form-submitting-state new file mode 100644 index 0000000000000..55be31d3a5329 --- /dev/null +++ b/projects/packages/forms/changelog/update-contact-form-submitting-state @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Contact Form: added submitting state diff --git a/projects/packages/forms/composer.json b/projects/packages/forms/composer.json index 81d489dcdd212..bf4cf37f0be5e 100644 --- a/projects/packages/forms/composer.json +++ b/projects/packages/forms/composer.json @@ -62,7 +62,7 @@ "link-template": "https://github.com/automattic/jetpack-forms/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.24.x-dev" + "dev-trunk": "0.25.x-dev" }, "textdomain": "jetpack-forms", "version-constants": { diff --git a/projects/packages/forms/package.json b/projects/packages/forms/package.json index 64c2c69eab85f..e19018c01ae0a 100644 --- a/projects/packages/forms/package.json +++ b/projects/packages/forms/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@automattic/jetpack-forms", - "version": "0.24.3-alpha", + "version": "0.25.0-alpha", "description": "Jetpack Forms", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/forms/#readme", "bugs": { diff --git a/projects/packages/forms/src/blocks/contact-form/editor.scss b/projects/packages/forms/src/blocks/contact-form/editor.scss index cb56627227e7d..a1544ecd74678 100644 --- a/projects/packages/forms/src/blocks/contact-form/editor.scss +++ b/projects/packages/forms/src/blocks/contact-form/editor.scss @@ -441,6 +441,22 @@ margin: 0 5px 0 0; } } + + .jetpack-field-option { + &.field-option-checkbox, + &.field-option-radio { + display: flex; + align-items: baseline; /* Align input with first label line */ + + .jetpack-option__type:before { + display: block; /* display: flex causes baselines to not align */ + } + } + + &.field-option-radio .jetpack-option__type { + transform: translateY(15%); /* Small offset to compensate the slightly odd perceived alignment that's due to the circular shape */ + } + } } :where(:not(.contact-form)) > .jetpack-field { @@ -529,24 +545,33 @@ .jetpack-field-checkbox, .jetpack-field-consent { display: flex; + + .jetpack-field-label .jetpack-field-label__input { + font-weight: 400; + } +} + +.jetpack-field-checkbox { + align-items: baseline; + + .jetpack-field-label { + display: block; + } +} + +.jetpack-field-consent { align-items: center; .jetpack-field-label { flex-grow: 1; .jetpack-field-label__input { - font-weight: 400; + font-size: 13px; + text-transform: uppercase; } } } -.jetpack-field-consent { - .jetpack-field-label__input { - font-size: 13px; - text-transform: uppercase; - } -} - // Overrides to make the preview look good .block-editor-inserter__preview { .jetpack-contact-form { diff --git a/projects/packages/forms/src/class-jetpack-forms.php b/projects/packages/forms/src/class-jetpack-forms.php index 7ed82801f9cd9..5b22439f159e2 100644 --- a/projects/packages/forms/src/class-jetpack-forms.php +++ b/projects/packages/forms/src/class-jetpack-forms.php @@ -15,7 +15,7 @@ */ class Jetpack_Forms { - const PACKAGE_VERSION = '0.24.3-alpha'; + const PACKAGE_VERSION = '0.25.0-alpha'; /** * Load the contact form module. diff --git a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php index 7923c1810a0f3..eec7ce4cb30c6 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php @@ -259,7 +259,8 @@ protected function __construct() { './js/accessible-form.js', __FILE__, array( - 'async' => true, + 'async' => true, + 'version' => \JETPACK__VERSION, ) ); @@ -273,6 +274,8 @@ protected function __construct() { 'invalidForm' => __( 'Please make sure all fields are valid.', 'jetpack-forms' ), /* translators: error message shown when a multiple choice field requires at least one option to be selected. */ 'checkboxMissingValue' => __( 'Please select at least one option.', 'jetpack-forms' ), + /* translators: text read by a screen reader when a form is being submitted */ + 'submittingForm' => __( 'Submitting form', 'jetpack-forms' ), ) ); diff --git a/projects/packages/forms/src/contact-form/css/grunion.css b/projects/packages/forms/src/contact-form/css/grunion.css index 9f0ad4dee403a..9750eb4f4ca95 100644 --- a/projects/packages/forms/src/contact-form/css/grunion.css +++ b/projects/packages/forms/src/contact-form/css/grunion.css @@ -80,7 +80,6 @@ .contact-form input[type='checkbox'] { width: 1rem; height: 1rem; - float: none; margin: 0 0.75rem 0 0; } @@ -115,11 +114,8 @@ .contact-form label.checkbox-multiple, .contact-form label.radio { margin-bottom: 0; - float: none; font-weight: normal; - display: inline-flex; - align-items: center; - line-height: 1; + flex: 1; } .contact-form .grunion-checkbox-multiple-options, @@ -160,6 +156,7 @@ .contact-form .grunion-checkbox-multiple-options .contact-form-field, .contact-form .grunion-radio-options .contact-form-field { display: flex; + align-items: baseline; margin: 0; } @@ -633,65 +630,51 @@ on production builds, the attributes are being reordered, causing side-effects .contact-form .grunion-field-wrap input.checkbox-multiple, .contact-form .grunion-field-wrap input.radio { - align-items: center; - appearance: none; - color: var(--jetpack--contact-form--text-color); - border: none; - font-size: var(--jetpack--contact-form--font-size); - font-family: var(--jetpack--contact-form--font-family); - height: var(--jetpack--contact-form--font-size); - justify-content: center; - margin: 0 10px 0 0; - padding: 0; position: relative; - width: var(--jetpack--contact-form--font-size); -} -.contact-form .grunion-field-wrap input.checkbox-multiple::after, -.contact-form .grunion-field-wrap input.checkbox-multiple::before, -.contact-form .grunion-field-wrap input.radio::after, -.contact-form .grunion-field-wrap input.radio::before { - all: initial; - color: inherit; - font-size: inherit; - font-family: inherit; -} - -.contact-form .grunion-field-wrap input.checkbox-multiple::before, -.contact-form .grunion-field-wrap input.radio::before { - align-items: center; - border-color: currentColor; - border-radius: min(var(--jetpack--contact-form--button-outline--border-radius, 0px), 4px); - border-style: solid; - border-width: 1px; box-sizing: border-box; - content: ' '; - display: flex; - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; - font-weight: 400; - height: var(--jetpack--contact-form--font-size); - justify-content: center; width: var(--jetpack--contact-form--font-size); + height: var(--jetpack--contact-form--font-size); + margin-inline-end: calc(var(--jetpack--contact-form--font-size) / 2); + padding: 0; + + appearance: none; + border: solid 1px var(--jetpack--contact-form--text-color); + + outline-offset: 4px; } -.contact-form .grunion-field-wrap input.radio::before { +.contact-form .grunion-field-wrap input.radio { border-radius: 50%; + + transform: translateY(15%); /* Small offset to compensate the slightly odd perceived alignment that's due to the circular shape */ } .contact-form .grunion-field-wrap input.checkbox-multiple:checked::before { content: "\2713"; + + position: absolute; + top: calc(var(--jetpack--contact-form--font-size) / 2); + left: calc(var(--jetpack--contact-form--font-size) / 2); + + display: block; + + font-size: var(--jetpack--contact-form--font-size); + line-height: 1; + + transform: translate(-50%, -50%); } -.contact-form .grunion-field-wrap input.radio:checked::after { +.contact-form .grunion-field-wrap input.radio:checked::before { background: currentColor; border-radius: 50%; content: ''; - height: 0.5em; + height: calc(var(--jetpack--contact-form--font-size) / 2); + margin-left: 50%; + margin-top: 50%; position: absolute; transform: translate(-50%, -50%); - width: 0.5em; - top: calc(var(--jetpack--contact-form--font-size) / 2); - inset-inline-start: calc(var(--jetpack--contact-form--font-size) / 2); + width: calc(var(--jetpack--contact-form--font-size) / 2); } .contact-form .grunion-field-wrap.is-style-button-wrap .grunion-checkbox-multiple-label, @@ -784,13 +767,13 @@ on production builds, the attributes are being reordered, causing side-effects .contact-form__input-error { display: flex; align-items: center; - gap: 0.25em; + gap: 0.33em; font-size: 1rem; } .contact-form__error { - margin-bottom: 0.25rem; + margin-bottom: var(--wp--style--block-gap, 1.5rem); padding: 0.5em; background-color: var(--jetpack--contact-form--error-color); @@ -818,7 +801,53 @@ on production builds, the attributes are being reordered, causing side-effects outline-offset: 0.5em; } +.contact-form__warning-icon { + display: inline-flex; + justify-content: center; + align-items: center; + + width: 1.25em; + height: 1.25em; + + background-color: var(--jetpack--contact-form--error-color); + border-radius: 50%; + border: solid 1px var(--jetpack--contact-form--inverted-text-color); + color: var(--jetpack--contact-form--inverted-text-color); +} + +.contact-form__warning-icon::after { + content: "\0021"; + + font-size: 0.8em; + font-weight: bold; +} + .contact-form__checkbox-wrap { display: inline-flex; align-items: baseline; +} + +.contact-form :is([type="submit"],button:not([type="reset"])) { + display: inline-flex; + align-items: center; + gap: 0.5em; +} + +.contact-form .contact-form__spinner { + fill: currentColor; +} + +.contact-form .contact-form__spinner svg { + /* Ensure container is of the exact same dimension */ + display: block; +} + +.visually-hidden { + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; } \ No newline at end of file diff --git a/projects/packages/forms/src/contact-form/js/accessible-form.js b/projects/packages/forms/src/contact-form/js/accessible-form.js index fe9191a89569a..2ebd308ca44f6 100644 --- a/projects/packages/forms/src/contact-form/js/accessible-form.js +++ b/projects/packages/forms/src/contact-form/js/accessible-form.js @@ -1,5 +1,10 @@ /** * @file Overwrites native form validation to provide an accessible experience to all users. + * + * In the code below, be aware that the terms "input" and "field" mean different things. An input + * refers to a UI element a user can interact with, such as a text field or a checbkox. A field + * represents a question of a form and can hold multiple inputs, such as the Single Choice + * (multiple radio buttons) or Multiple Choice fields (multiple checkboxes). */ const L10N = window.jetpackContactForm || {}; @@ -22,7 +27,7 @@ const initAllForms = () => { }; /** - * Register event listeners on the specified form and disable native validation. + * Implement a form custom validation. * @param {HTMLFormElement} form Form element */ const initForm = form => { @@ -32,77 +37,283 @@ const initForm = form => { form.setAttribute( 'novalidate', true ); } + const opts = { + hasInsetLabel: hasFormInsetLabels( form ), + }; + + // Hold references to the input event listeners. + let inputListenerMap = {}; + form.addEventListener( 'submit', e => { - clearErrors( form ); + e.preventDefault(); - if ( form.checkValidity() ) { - const submitBtn = getFormSubmitBtn( form ); + // Prevent multiple submissions. + if ( isFormSubmitting( form ) ) { + return; + } - if ( submitBtn ) { - // TODO: implement loading state - // Temporarily prevents the user from submitting the form multiple times. - submitBtn.disabled = true; - } - } else { - e.preventDefault(); + clearForm( form, inputListenerMap, opts ); + + if ( isFormValid( form ) ) { + inputListenerMap = {}; - setErrors( form ); + submitForm( form ); + } else { + inputListenerMap = invalidateForm( form, opts ); } } ); }; +/****************************************************************************** + * CHECKS + ******************************************************************************/ + +/** + * Check if a form has valid entries. + * @param {HTMLFormElement} form FormElement + * @returns {boolean} + */ +const isFormValid = form => { + let isValid = form.checkValidity(); + + if ( ! isValid ) { + return false; + } + + // Handle the Multiple Choice fields separately since checkboxes can't have a required attribute + // in that case. + const multipleChoiceFields = getMultipleChoiceFields( form ); + + for ( const field of multipleChoiceFields ) { + if ( isMultipleChoiceFieldRequired( field ) && ! isMultipleChoiceFieldValid( field ) ) { + return false; + } + } + + return isValid; +}; + +/** + * Check if a form is submitting. + * @param {HTMLFormElement} form Form element + * @returns {boolean} + */ +const isFormSubmitting = form => { + return form.getAttribute( 'data-submitting' ) === true; +}; + +/** + * Check if an element is a Multiple Choice field (i.e., a fieldset with checkboxes). + * @param {HTMLElement} elt Element + * @returns {boolean} + */ +const isMultipleChoiceField = elt => { + return ( + elt.tagName.toLowerCase() === 'fieldset' && + elt.classList.contains( 'grunion-checkbox-multiple-options' ) + ); +}; + +/** + * Check if an element is a Single Choice field (i.e., a fieldset with radio buttons). + * @param {HTMLElement} elt Element + * @returns {boolean} + */ +const isSingleChoiceField = elt => { + return ( + elt.tagName.toLowerCase() === 'fieldset' && elt.classList.contains( 'grunion-radio-options' ) + ); +}; + +/** + * Check if a Multiple Choice field is required. + * @param {HTMLFieldSetElementi} fieldset Fieldset element + * @returns {boolean} + */ +const isMultipleChoiceFieldRequired = fieldset => { + // Unlike radio buttons, we can't use the `required` attribute on checkboxes. + return fieldset.hasAttribute( 'data-required' ); +}; + +/** + * Check if a Single Choice field is required. + * @param {HTMLFieldSetElementi} fieldset Fieldset element + * @returns {boolean} + */ +const isSingleChoiceFieldRequired = fieldset => { + return Array.from( fieldset.querySelectorAll( 'input[type="radio"]' ) ).some( + input => input.hasAttribute( 'required' ) || input.hasAttribute( 'aria-required' ) + ); +}; + +/** + * Check if a simple field (with a single input) is valid. + * @param {HTMLElement} input Field input element + * @returns {boolean} + */ +const isSimpleFieldValid = input => { + return input.validity.valid; +}; + +/** + * Check if a required single choice field (with radio buttons) is valid. + * @param {HTMLFieldSetElement} fieldset Fieldset element + * @returns {boolean} + */ +const isSingleChoiceFieldValid = fieldset => { + const inputs = Array.from( fieldset.querySelectorAll( 'input[type="radio"]' ) ); + + if ( inputs.length > 0 ) { + return inputs.every( input => input.validity.valid ); + } + + return false; +}; + +/** + * Check if a required multiple choice field (with checkboxes) is valid. + * @param {HTMLFieldSetElement} fieldset Fieldset element + * @returns {boolean} + */ +const isMultipleChoiceFieldValid = fieldset => { + if ( ! isMultipleChoiceFieldRequired( fieldset ) ) { + return true; + } + + const inputs = Array.from( fieldset.querySelectorAll( 'input[type="checkbox"]' ) ); + + if ( inputs.length > 0 ) { + return inputs.some( input => input.checked ); + } + + return false; +}; + +/** + * Return whether a form has inset labels (like with the Outlined and Animates styles). + * @param {HTMLFormElement} form Form element + * @returns {boolean} + */ +const hasFormInsetLabels = form => { + // The style "container" is insde the form. + const block = form.querySelector( '.wp-block-jetpack-contact-form' ); + + if ( ! block ) { + return false; + } + + const blockClassList = block.classList; + + return ( + blockClassList.contains( 'is-style-outlined' ) || blockClassList.contains( 'is-style-animated' ) + ); +}; + /****************************************************************************** * GETTERS ******************************************************************************/ /** - * Return the submit button of the specified form. + * Return the submit button of a form. * @param {HTMLFormElement} form Form element * @returns {HTMLButtonElement|HTMLInputElement|undefined} Submit button */ const getFormSubmitBtn = form => { return ( - form.querySelector( 'input[type="submit"]' ) || - form.querySelector( 'button:not([type="reset"])' ) + form.querySelector( '[type="submit"]' ) || form.querySelector( 'button:not([type="reset"])' ) ); }; /** - * Return the inputs of the specified form. + * Return the Multiple Choice fields of a form. + * @param {HTMLFormElement} form Form element + * @returns {HTMLFieldSetElement[]} Fieldset elements + */ +const getMultipleChoiceFields = form => { + return Array.from( form.querySelectorAll( '.grunion-checkbox-multiple-options' ) ); +}; + +/** + * Return the inputs of a specified form. * @param {HTMLFormElement} form Form element - * @returns {NodeListOf} Form inputs + * @returns {HTMLElement[]} Form inputs */ const getFormInputs = form => { - return [ ...form.elements ].filter( + return Array.from( form.elements ).filter( // input.offsetParent filters out inputs of which the parent is hidden. input => ! [ 'hidden', 'submit' ].includes( input.type ) && input.offsetParent !== null ); }; /** - * Return the error element associated to the specified form. + * Get the fields of a form. * @param {HTMLFormElement} form Form element - * @returns {HTMLElement|undefined} Error element + * @returns {object} Form fields (type: fields[]) */ -const getFormError = form => { - return form.querySelector( '.contact-form__error' ); +const getFormFields = form => { + const groupedInputs = groupInputs( getFormInputs( form ) ); + const fields = { + simple: groupedInputs.default, + singleChoice: [], + multipleChoice: [], + }; + + // Single Choice fields (i.e., fieldsets with radio buttons) + const uniqueRadioNames = groupedInputs.radios.reduce( + ( acc, input ) => ( acc.includes( input.name ) ? acc : [ ...acc, input.name ] ), + [] + ); + + for ( const name of uniqueRadioNames ) { + // Get the first radio button of the group. + const input = form.querySelector( `input[type="radio"][name="${ name }"]` ); + + if ( input ) { + const fieldset = input.closest( 'fieldset' ); + + if ( fieldset ) { + fields.singleChoice.push( fieldset ); + } + } + } + + // Multiple Choice fields (i.e., fieldsets with checkboxes) + const uniqueCheckboxNames = groupedInputs.checkboxes.reduce( + ( acc, input ) => ( acc.includes( input.name ) ? acc : [ ...acc, input.name ] ), + [] + ); + + for ( const name of uniqueCheckboxNames ) { + // Get the first checkbox of the group. + const input = form.querySelector( `input[type="checkbox"][name="${ name }"]` ); + + if ( input ) { + const fieldset = input.closest( 'fieldset' ); + + if ( fieldset ) { + fields.multipleChoice.push( fieldset ); + } + } + } + + return fields; }; /** - * Return the error elements associated to the inputs of the specified form. + * Return the error element of a form. * @param {HTMLFormElement} form Form element - * @returns {NodeListOf} Error elements + * @returns {HTMLElement|undefined} Error element */ -const getFormInputErrors = form => { - return form.querySelectorAll( '.contact-form__input-error' ); +const getFormError = form => { + return form.querySelector( '.contact-form__error' ); }; /** - * Return the elements marked as invalid in the specified form. + * Return the fields marked as invalid in a form. * @param {HTMLFormElement} form Form element * @returns {NodeListOf} Invalid elements */ -const getFormInvalidFields = form => { +const getInvalidFields = form => { return form.querySelectorAll( '[aria-invalid]' ); }; @@ -110,6 +321,33 @@ const getFormInvalidFields = form => { * BUILDERS ******************************************************************************/ +/** + * Create a new spinner. + * @returns {HTMLSpanElement} Spinner + */ +const createSpinner = () => { + const elt = document.createElement( 'span' ); + const spinner = document.createElement( 'span' ); + const srText = document.createElement( 'span' ); + + // Hide SVG from screen readers + spinner.setAttribute( 'aria-hidden', true ); + // Inlining the SVG rather than embedding it in an tag allows us to set the `fill` property + // in CSS. + spinner.innerHTML = + ''; + + // Spinner replacement for screen readers + srText.classList.add( 'visually-hidden' ); + srText.textContent = L10N.submittingForm || 'Submitting form'; + + elt.classList.add( 'contact-form__spinner' ); + elt.appendChild( spinner ); + elt.appendChild( srText ); + + return elt; +}; + /** * Create a new warning icon. * @returns {HTMLSpanElement} Warning icon @@ -117,7 +355,7 @@ const getFormInvalidFields = form => { const createWarningIcon = () => { const elt = document.createElement( 'span' ); - elt.classList.add( 'dashicons', 'dashicons-warning' ); + elt.classList.add( 'contact-form__warning-icon' ); elt.setAttribute( 'aria-label', L10N.warning || 'Warning' ); return elt; @@ -166,11 +404,10 @@ const createFormErrorContainer = () => { /** * Create a new error container for a form input. - * @param {HTMLElement} input Input element * @param {string} errorId Error element ID * @returns {HTMLDivElement} Error container */ -const createFormInputErrorContainer = ( input, errorId ) => { +const createInputErrorContainer = errorId => { const elt = document.createElement( 'div' ); elt.id = errorId; @@ -183,33 +420,14 @@ const createFormInputErrorContainer = ( input, errorId ) => { * UTILS ******************************************************************************/ -/** - * Return whether the form has inset labels (like with the Outlined and Animates styles). - * @param {HTMLFormElement} form Form element - * @returns {boolean} - */ -const hasFormInsetLabels = form => { - const block = form.querySelector( '.wp-block-jetpack-contact-form' ); - - if ( ! block ) { - return; - } - - const blockClassList = block.classList; - - return ( - blockClassList.contains( 'is-style-outlined' ) || blockClassList.contains( 'is-style-animated' ) - ); -}; - /** * Group radio inputs and checkbox inputs with multiple values. * Single inputs, checkbox groups and radio buttons handle validation and error * messages differently. - * @param {NodeListOf} inputs Form inputs + * @param {HTMLElement[]} inputs Form inputs * @returns {object} Grouped inputs */ -const groupFormInputs = inputs => { +const groupInputs = inputs => { return inputs.reduce( ( acc, input ) => { switch ( input.type ) { @@ -236,134 +454,362 @@ const groupFormInputs = inputs => { }; /****************************************************************************** - * DOM UPDATES + * CLEANUP ******************************************************************************/ /** - * Empty the error element of the specified form and its inputs and mark the latter as valid. + * Clear all errors and remove all input event listeners in a form. * @param {HTMLFormElement} form Form element + * @param {object} inputListenerMap Map of input event listeners (name: handler) + * @param {object} opts Form options */ -const clearErrors = form => { +const clearForm = ( form, inputListenerMap, opts ) => { + clearErrors( form, opts ); + + for ( const name in inputListenerMap ) { + form + .querySelectorAll( `[name="${ name }"]` ) + .forEach( input => input.removeEventListener( 'blur', inputListenerMap[ name ] ) ); + } +}; + +/** + * Remove the errors in a form. + * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + */ +const clearErrors = ( form, opts ) => { + clearFormError( form ); + clearFieldErrors( form, opts ); +}; + +/** + * Empty the error element of a form. + * @param {HTMLFormElement} form Form element + */ +const clearFormError = form => { const formError = getFormError( form ); if ( formError ) { - formError.textContent = ''; + formError.replaceChildren(); } +}; - for ( const inputError of getFormInputErrors( form ) ) { - inputError.textContent = ''; +/** + * Empty the error element of form fields and mark them as valid. + * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + */ +const clearFieldErrors = ( form, opts ) => { + for ( const field of getInvalidFields( form ) ) { + if ( isSingleChoiceField( field ) ) { + clearGroupInputError( field ); + } else if ( isMultipleChoiceField( field ) ) { + clearGroupInputError( field ); + } else { + clearInputError( field, opts ); + } } +}; - for ( const field of getFormInvalidFields( form ) ) { - field.removeAttribute( 'aria-invalid' ); - field.removeAttribute( 'aria-describedby' ); +/** + * Empty the error element of a field with multiple inputs (e.g., Multiple Choice or Single Choice + * fields) and mark it as valid. + * @param {HTMLFieldSetElement} fieldset Fieldset element + */ +const clearGroupInputError = fieldset => { + fieldset.removeAttribute( 'aria-invalid' ); + fieldset.removeAttribute( 'aria-describedby' ); + + const error = fieldset.querySelector( '.contact-form__input-error' ); + + if ( error ) { + error.replaceChildren(); } }; /** - * Set the errors of the specified form and its inputs. + * Empty the error element a simple field (unique input) and mark it as valid. + * @param {HTMLElement} input Input element + * @param {object} opts Form options + */ +const clearInputError = ( input, opts ) => { + input.removeAttribute( 'aria-invalid' ); + input.removeAttribute( 'aria-describedby' ); + + const fieldWrap = input.closest( + opts.hasInsetLabel ? '.contact-form__inset-label-wrap' : '.grunion-field-wrap' + ); + + if ( ! fieldWrap ) { + return; + } + + const error = fieldWrap.querySelector( '.contact-form__input-error' ); + + if ( error ) { + error.replaceChildren(); + } +}; + +/****************************************************************************** + * SUBMISSION + ******************************************************************************/ + +/** + * Submit a form and set its submitting state. * @param {HTMLFormElement} form Form element */ -const setErrors = form => { - setFormError( form ); - setFormInputErrors( form ); +const submitForm = form => { + showFormSubmittingIndicator( form ); + + form.setAttribute( 'data-submitting', true ); + form.submit(); }; /** - * Set the error element of the specified form. + * Show a spinner in the submit button of a form. * @param {HTMLFormElement} form Form element */ -const setFormError = form => { +const showFormSubmittingIndicator = form => { const submitBtn = getFormSubmitBtn( form ); - // Bail out, something's wrong with the form. - if ( ! submitBtn ) { - return; + if ( submitBtn ) { + // We should avoid using `disabled` when possible. One of the reasons is that `disabled` + // buttons lose their focus, which can be confusing. Better use `aria-disabled` instead. + // Ref. https://css-tricks.com/making-disabled-buttons-more-inclusive/#aa-aria-to-the-rescue + submitBtn.setAttribute( 'aria-disabled', true ); + submitBtn.appendChild( createSpinner() ); } +}; - let error = getFormError( form ); +/****************************************************************************** + * INVALIDATION + ******************************************************************************/ - if ( ! error ) { - error = createFormErrorContainer( form ); +/** + * Show errors in the form and trigger revalidation on inputs blur. + * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + * @returns {object} Map of the input event listeners set (name: handler) + */ +const invalidateForm = ( form, opts ) => { + setErrors( form, opts ); - submitBtn.parentNode.insertBefore( error, submitBtn ); + return listenToInvalidFields( form, opts ); +}; + +/** + * Trigger the fields revalidation on a form inputs blur. + * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + * @returns {object} Map of the input event listeners set (name: handler) + */ +const listenToInvalidFields = ( form, opts ) => { + let listenerMap = {}; + + const eventCb = () => updateFormErrorMessage( form ); + + for ( const field of getInvalidFields( form ) ) { + let obj; + + if ( isSingleChoiceField( field ) && isSingleChoiceFieldRequired( field ) ) { + obj = listenToInvalidSingleChoiceField( field, eventCb, form, opts ); + } else if ( isMultipleChoiceField( field ) && isMultipleChoiceFieldRequired( field ) ) { + obj = listenToInvalidMultipleChoiceField( field, eventCb, form, opts ); + } else { + obj = listenToInvalidSimpleField( field, eventCb, form, opts ); + } + + listenerMap = { + ...listenerMap, + ...obj, + }; } - error.appendChild( - createError( L10N.invalidForm || 'Please make sure all fields are correct.' ) - ); + return listenerMap; }; /** - * Set the error elements of the inputs of the specified form. + * Trigger the revalidation of a Single Choice field on its inputs blur. + * @param {HTMLFieldSetElement} fieldset Fieldset element + * @param {Function} cb Function to call on event * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + * @returns {object} Map of the input event listeners set (name: handler) */ -const setFormInputErrors = form => { - const opts = { - hasInsetLabel: hasFormInsetLabels( form ), +const listenToInvalidSingleChoiceField = ( fieldset, cb, form, opts ) => { + const listenerMap = {}; + const blurHandler = () => { + if ( isSingleChoiceFieldValid( fieldset ) ) { + clearGroupInputError( fieldset ); + } else { + setSingleChoiceFieldError( fieldset, form, opts ); + } + + cb(); }; - const groupedInputs = groupFormInputs( getFormInputs( form ) ); - // Handle individual inputs - for ( const input of groupedInputs.default ) { - if ( ! input.validity.valid ) { - setFormInputError( input, form, opts ); + const inputs = fieldset.querySelectorAll( 'input[type="radio"]' ); + + for ( const input of inputs ) { + input.addEventListener( 'blur', blurHandler ); + + listenerMap[ input.name ] = blurHandler; + } + + return listenerMap; +}; + +/** + * Trigger the revalidation of a Multiple Choice field on its inputs blur. + * @param {HTMLFieldSetElement} fieldset Fieldset element + * @param {Function} cb Function to call on event + * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + * @returns {object} Map of the input event listeners set (name: handler) + */ +const listenToInvalidMultipleChoiceField = ( fieldset, cb, form, opts ) => { + const listenerMap = {}; + const blurHandler = () => { + if ( isMultipleChoiceFieldValid( fieldset ) ) { + clearGroupInputError( fieldset ); + } else { + setMultipleChoiceFieldError( fieldset, form, opts ); } + + cb(); + }; + + const inputs = fieldset.querySelectorAll( 'input[type="checkbox"]' ); + + for ( const input of inputs ) { + input.addEventListener( 'blur', blurHandler ); + + listenerMap[ input.name ] = blurHandler; } - // Handle radio buttons - const radioButtonNames = groupedInputs.radios.reduce( - ( acc, input ) => ( acc.includes( input.name ) ? acc : [ ...acc, input.name ] ), - [] - ); + return listenerMap; +}; - for ( const name of radioButtonNames ) { - // Get the first radio button of the group. - const input = form.querySelector( `input[type="radio"][name="${ name }"]` ); +/** + * Trigger the revalidation of a simple field (single input) on its input blur. + * @param {HTMLElement} input Input element + * @param {Function} cb Function to call on event + * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + * @returns {object} Map of the input event listeners set (name: handler) + */ +const listenToInvalidSimpleField = ( input, cb, form, opts ) => { + const listenerMap = {}; + const blurHandler = () => { + if ( isSimpleFieldValid( input ) ) { + clearInputError( input, opts ); + } else { + setSimpleFieldError( input, form, opts ); + } + + cb(); + }; + + input.addEventListener( 'blur', blurHandler ); - // If one of the group radio buttons is checked, all radio buttons are valid. - if ( ! input.validity.valid ) { - setFormGroupInputError( input, form, opts ); + listenerMap[ input.name ] = blurHandler; + + return listenerMap; +}; + +/****************************************************************************** + * ERRORS + ******************************************************************************/ + +/** + * Set form errors. + * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + */ +const setErrors = ( form, opts ) => { + setFormError( form ); + setFieldErrors( form, opts ); +}; + +/** + * Set the error element of a form. + * @param {HTMLFormElement} form Form element + */ +const setFormError = form => { + let error = getFormError( form ); + + if ( ! error ) { + error = createFormErrorContainer( form ); + + const submitBtn = getFormSubmitBtn( form ); + + if ( submitBtn ) { + submitBtn.parentNode.insertBefore( error, submitBtn ); + } else { + form.appendChild( error ); } } - // Handle checkbox groups - const checkboxNames = groupedInputs.checkboxes.reduce( - ( acc, input ) => ( acc.includes( input.name ) ? acc : [ ...acc, input.name ] ), - [] + error.appendChild( + createError( L10N.invalidForm || 'Please make sure all fields are correct.' ) ); +}; - for ( const name of checkboxNames ) { - // Get the first checkbox of the group. - const input = form.querySelector( `input[type="checkbox"][name="${ name }"]` ); - const fieldset = input.closest( 'fieldset' ); - const isRequired = fieldset && fieldset.hasAttribute( 'data-required' ); +/** + * Update the error message of a form based on its validity. + * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + */ +const updateFormErrorMessage = form => { + clearFormError( form ); - if ( isRequired ) { - const formData = new FormData( form ); + if ( ! form.checkValidity() ) { + setFormError( form ); + } +}; - if ( formData.getAll( name ).length === 0 ) { - setFormGroupInputError( input, form, { - ...opts, - message: L10N.checkboxMissingValue || 'Please select at least one option.', - } ); - } +/** + * Set the error element of a form fields. + * @param {HTMLFormElement} form Form element + * @param {object} opts Form options + */ +const setFieldErrors = ( form, opts ) => { + const { simple, singleChoice, multipleChoice } = getFormFields( form ); + + for ( const field of simple ) { + if ( ! isSimpleFieldValid( field ) ) { + setSimpleFieldError( field, form, opts ); + } + } + + for ( const field of singleChoice ) { + if ( ! isSingleChoiceFieldValid( field ) ) { + setSingleChoiceFieldError( field, form, opts ); + } + } + + for ( const field of multipleChoice ) { + if ( ! isMultipleChoiceFieldValid( field ) ) { + setMultipleChoiceFieldError( field, form, opts ); } } }; /** - * Set the error element of the specified input. + * Set the error element of a simple field (single input) and mark it as invalid. * @param {HTMLElement} input Input element * @param {HTMLFormElement} form Parent form element - * @param {object} opts Options + * @param {object} opts Form options */ -const setFormInputError = ( input, form, opts ) => { +const setSimpleFieldError = ( input, form, opts ) => { const errorId = `${ input.name }-error`; + let error = form.querySelector( `#${ errorId }` ); if ( ! error ) { - error = createFormInputErrorContainer( input, errorId ); + error = createInputErrorContainer( errorId ); const wrap = input.closest( opts.hasInsetLabel ? '.contact-form__inset-label-wrap' : '.grunion-field-wrap' @@ -380,30 +826,56 @@ const setFormInputError = ( input, form, opts ) => { input.setAttribute( 'aria-describedby', errorId ); }; +/** + * Set the error element of a Single Choice field. + * @param {HTMLFieldSetElement} fieldset Fieldset element + * @param {HTMLFormElement} form Parent form element + * @param {object} opts Form options + */ +const setSingleChoiceFieldError = ( fieldset, form, opts ) => { + setGroupInputError( fieldset, form, opts ); +}; + +/** + * Set the error element of a Multiple Choice field. + * @param {HTMLFieldSetElement} fieldset Fieldset element + * @param {HTMLFormElement} form Parent form element + * @param {object} opts Form options + */ +const setMultipleChoiceFieldError = ( fieldset, form, opts ) => { + setGroupInputError( fieldset, form, { + ...opts, + message: L10N.checkboxMissingValue || 'Please select at least one option.', + } ); +}; + /** * Set the error element of a group of inputs, i.e. a group of radio buttons or checkboxes. * These types of inputs are handled differently because the error message and invalidity * apply to the group as a whole, not to each individual input. - * @param {HTMLElement} input An input element of the group + * @param {HTMLFieldSetElement} fieldset Fieldset element * @param {HTMLFormElement} form Parent form element * @param {object} opts Options */ -const setFormGroupInputError = ( input, form, opts ) => { - const errorId = `${ input.name.replace( '[]', '' ) }-error`; - let error = form.querySelector( `#${ errorId }` ); +const setGroupInputError = ( fieldset, form, opts ) => { + const firstInput = fieldset.querySelector( 'input' ); - if ( ! error ) { - error = createFormInputErrorContainer( input, errorId ); + if ( ! firstInput ) { + return; } - error.replaceChildren( createError( input.validationMessage || opts.message || 'Error' ) ); + const inputName = firstInput.name.replace( '[]', '' ); + const errorId = `${ inputName }-error`; - const fieldset = input.closest( 'fieldset' ); + let error = form.querySelector( `#${ errorId }` ); - if ( fieldset ) { - // Add the error after all the inputs. - fieldset.appendChild( error ); - fieldset.setAttribute( 'aria-invalid', 'true' ); - fieldset.setAttribute( 'aria-describedby', errorId ); + if ( ! error ) { + error = createInputErrorContainer( errorId ); } + + error.replaceChildren( createError( firstInput.validationMessage || opts.message || 'Error' ) ); + + fieldset.appendChild( error ); + fieldset.setAttribute( 'aria-invalid', 'true' ); + fieldset.setAttribute( 'aria-describedby', errorId ); }; diff --git a/projects/packages/identity-crisis/CHANGELOG.md b/projects/packages/identity-crisis/CHANGELOG.md index 8ec9850244866..53bd194019815 100644 --- a/projects/packages/identity-crisis/CHANGELOG.md +++ b/projects/packages/identity-crisis/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.14.0] - 2023-12-06 +### Added +- Send a verifcation secret when URL is IP. [#34436] + +### Changed +- Updated package dependencies. [#34416] + ## [0.13.0] - 2023-12-03 ### Added - Store for persistent blog ID for multi-URL purposes. [#34262] @@ -458,6 +465,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated package dependencies. - Use Connection/Urls for home_url and site_url functions migrated from Sync. +[0.14.0]: https://github.com/Automattic/jetpack-identity-crisis/compare/v0.13.0...v0.14.0 [0.13.0]: https://github.com/Automattic/jetpack-identity-crisis/compare/v0.12.1...v0.13.0 [0.12.1]: https://github.com/Automattic/jetpack-identity-crisis/compare/v0.12.0...v0.12.1 [0.12.0]: https://github.com/Automattic/jetpack-identity-crisis/compare/v0.11.3...v0.12.0 diff --git a/projects/packages/identity-crisis/changelog/renovate-babel-monorepo b/projects/packages/identity-crisis/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/packages/identity-crisis/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/packages/identity-crisis/composer.json b/projects/packages/identity-crisis/composer.json index 454133fff9ea9..1700a62a87fc9 100644 --- a/projects/packages/identity-crisis/composer.json +++ b/projects/packages/identity-crisis/composer.json @@ -66,7 +66,7 @@ "link-template": "https://github.com/Automattic/jetpack-identity-crisis/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.13.x-dev" + "dev-trunk": "0.14.x-dev" } }, "config": { diff --git a/projects/packages/identity-crisis/package.json b/projects/packages/identity-crisis/package.json index d5566f1a96aba..6d70c9e634a40 100644 --- a/projects/packages/identity-crisis/package.json +++ b/projects/packages/identity-crisis/package.json @@ -1,6 +1,6 @@ { "name": "jetpack-identity-crisis", - "version": "0.13.1-alpha", + "version": "0.14.0", "description": "Jetpack Identity Crisis", "main": "_inc/admin.jsx", "repository": { diff --git a/projects/packages/identity-crisis/src/class-identity-crisis.php b/projects/packages/identity-crisis/src/class-identity-crisis.php index bf719c1036452..9abaaac4e5bb3 100644 --- a/projects/packages/identity-crisis/src/class-identity-crisis.php +++ b/projects/packages/identity-crisis/src/class-identity-crisis.php @@ -27,7 +27,7 @@ class Identity_Crisis { /** * Package Version */ - const PACKAGE_VERSION = '0.13.1-alpha'; + const PACKAGE_VERSION = '0.14.0'; /** * Persistent WPCOM blog ID that stays in the options after disconnect. @@ -223,6 +223,10 @@ public function add_idc_query_args_to_url( $url ) { $query_args['migrate_for_idc'] = true; } + if ( self::url_is_ip() ) { + $query_args['url_secret'] = URL_Secret::create_secret( 'URL_argument_secret_failed' ); + } + if ( is_multisite() ) { $query_args['multisite'] = true; } @@ -1352,6 +1356,17 @@ public static function add_secret_to_url_validation_response( array $response ) return $response; } + /** + * Check if URL is an IP. + * + * @return bool + */ + public static function url_is_ip() { + $hostname = wp_parse_url( Urls::site_url(), PHP_URL_HOST ); + $is_ip = filter_var( $hostname, FILTER_VALIDATE_IP ) !== false ? true : false; + return $is_ip; + } + /** * Add IDC-related data to the registration query. * @@ -1363,20 +1378,9 @@ public static function register_request_body( array $params ) { $persistent_blog_id = get_option( static::PERSISTENT_BLOG_ID_OPTION_NAME ); if ( $persistent_blog_id ) { $params['persistent_blog_id'] = $persistent_blog_id; - - $hostname = wp_parse_url( Urls::site_url(), PHP_URL_HOST ); - if ( filter_var( $hostname, FILTER_VALIDATE_IP ) !== false ) { - try { - $secret = new URL_Secret(); - $secret->create(); - - if ( $secret->exists() ) { - $params['url_secret'] = $secret->get_secret(); - } - } catch ( Exception $e ) { - // No need to stop the registration flow, just track the error and proceed. - ( new Tracking() )->record_user_event( 'registration_request_url_secret_failed', array( 'current_url' => Urls::site_url() ) ); - } + // If URL is IP, add secret to the request. + if ( self::url_is_ip() ) { + $params['url_secret'] = URL_Secret::create_secret( 'registration_request_url_secret_failed' ); } } diff --git a/projects/packages/identity-crisis/src/class-url-secret.php b/projects/packages/identity-crisis/src/class-url-secret.php index f7a9d209393b3..3d2f453cef5a6 100644 --- a/projects/packages/identity-crisis/src/class-url-secret.php +++ b/projects/packages/identity-crisis/src/class-url-secret.php @@ -131,4 +131,27 @@ public function exists() { private function generate_secret() { return wp_generate_password( 12, false ); } + + /** + * Generate secret for response. + * + * @param string $flow used to tell which flow generated the exception. + * @return string + */ + public static function create_secret( $flow = 'generating_secret_failed' ) { + $secret = null; + try { + + $secret = new self(); + $secret->create(); + + if ( $secret->exists() ) { + $secret = $secret->get_secret(); + } + } catch ( Exception $e ) { + // Track the error and proceed. + ( new Tracking() )->record_user_event( $flow, array( 'current_url' => Urls::site_url() ) ); + } + return $secret; + } } diff --git a/projects/packages/jetpack-mu-wpcom/changelog/feat-assembler-first-launchpad-task b/projects/packages/jetpack-mu-wpcom/changelog/feat-assembler-first-launchpad-task index 1a1a9bcee1503..4f408a8c4d8fe 100644 --- a/projects/packages/jetpack-mu-wpcom/changelog/feat-assembler-first-launchpad-task +++ b/projects/packages/jetpack-mu-wpcom/changelog/feat-assembler-first-launchpad-task @@ -1,4 +1,4 @@ Significance: minor Type: added -Launchpad: Add tasks for the new assembler-first flow +Launchpad: Set up tasks for the new assembler-first flow diff --git a/projects/packages/jetpack-mu-wpcom/changelog/update-earn-launchpad-stripe-return b/projects/packages/jetpack-mu-wpcom/changelog/update-earn-launchpad-stripe-return new file mode 100644 index 0000000000000..19574f462e913 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/update-earn-launchpad-stripe-return @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Launchpad: Add source to Earn stripe task diff --git a/projects/packages/jetpack-mu-wpcom/src/features/launchpad/launchpad-task-definitions.php b/projects/packages/jetpack-mu-wpcom/src/features/launchpad/launchpad-task-definitions.php index 3bd8025cdcdd4..222ef5bdf8065 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/launchpad/launchpad-task-definitions.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/launchpad/launchpad-task-definitions.php @@ -504,7 +504,8 @@ function wpcom_launchpad_get_task_definitions() { if ( function_exists( 'get_memberships_connected_account_redirect' ) ) { return get_memberships_connected_account_redirect( get_current_user_id(), - get_current_blog_id() + get_current_blog_id(), + 'earn-launchpad' ); } return '/earn/payments/' . $data['site_slug_encoded']; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/launchpad/launchpad.php b/projects/packages/jetpack-mu-wpcom/src/features/launchpad/launchpad.php index d7f7f0f72c64e..eef92be09450f 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/launchpad/launchpad.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/launchpad/launchpad.php @@ -248,7 +248,7 @@ function wpcom_launchpad_get_task_list_definitions() { }, 'task_ids' => array( 'verify_domain_email', - 'plan_selected', + 'plan_completed', 'setup_free', 'design_selected', 'domain_upsell', diff --git a/projects/packages/my-jetpack/CHANGELOG.md b/projects/packages/my-jetpack/CHANGELOG.md index b07e57c637045..963ecd32bb116 100644 --- a/projects/packages/my-jetpack/CHANGELOG.md +++ b/projects/packages/my-jetpack/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.1.2] - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + +### Fixed +- Creator Card: fix typo. [#34478] + +## [4.1.1] - 2023-12-05 +### Fixed +- My Jetpack: Fix outdated product cache issue when enabling tiers. [#34428] + ## [4.1.0] - 2023-12-03 ### Added - Added Jetpack Creator to My Jetpack. [#34307] @@ -1143,6 +1154,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Created package +[4.1.2]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.1.1...4.1.2 +[4.1.1]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.1.0...4.1.1 [4.1.0]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.0.3...4.1.0 [4.0.3]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.0.2...4.0.3 [4.0.2]: https://github.com/Automattic/jetpack-my-jetpack/compare/4.0.1...4.0.2 diff --git a/projects/packages/my-jetpack/changelog/renovate-babel-monorepo b/projects/packages/my-jetpack/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/packages/my-jetpack/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/packages/my-jetpack/changelog/update-my-jetpack-handle-product-cache-on-jetpack-ai b/projects/packages/my-jetpack/changelog/update-my-jetpack-handle-product-cache-on-jetpack-ai deleted file mode 100644 index 00a63eb735af1..0000000000000 --- a/projects/packages/my-jetpack/changelog/update-my-jetpack-handle-product-cache-on-jetpack-ai +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fixed - -My Jetpack: Fix outdated product cache issue when enabling tiers. diff --git a/projects/packages/my-jetpack/package.json b/projects/packages/my-jetpack/package.json index fa2ceaed721c4..09169e707cd40 100644 --- a/projects/packages/my-jetpack/package.json +++ b/projects/packages/my-jetpack/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@automattic/jetpack-my-jetpack", - "version": "4.1.1-alpha", + "version": "4.1.2", "description": "WP Admin page with information and configuration shared among all Jetpack stand-alone plugins", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/my-jetpack/#readme", "bugs": { diff --git a/projects/packages/my-jetpack/src/class-initializer.php b/projects/packages/my-jetpack/src/class-initializer.php index 603da02212fbf..57124f06f48f6 100644 --- a/projects/packages/my-jetpack/src/class-initializer.php +++ b/projects/packages/my-jetpack/src/class-initializer.php @@ -32,7 +32,7 @@ class Initializer { * * @var string */ - const PACKAGE_VERSION = '4.1.1-alpha'; + const PACKAGE_VERSION = '4.1.2'; /** * HTML container ID for the IDC screen on My Jetpack page. diff --git a/projects/packages/my-jetpack/src/products/class-creator.php b/projects/packages/my-jetpack/src/products/class-creator.php index c7578adb15cec..a4d1c471bc011 100644 --- a/projects/packages/my-jetpack/src/products/class-creator.php +++ b/projects/packages/my-jetpack/src/products/class-creator.php @@ -210,7 +210,7 @@ public static function get_features_by_tier() { ), ), array( - 'name' => __( 'Newsltter', 'jetpack-my-jetpack' ), + 'name' => __( 'Newsletter', 'jetpack-my-jetpack' ), 'info' => array( 'content' => __( 'Start a Newsletter by sending your content as an email newsletter direct to your fans email inboxes.', diff --git a/projects/packages/stats/changelog/add-top-posts-pages-block b/projects/packages/stats/changelog/add-top-posts-pages-block new file mode 100644 index 0000000000000..16d829abbce10 --- /dev/null +++ b/projects/packages/stats/changelog/add-top-posts-pages-block @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Permit overriding cache when retrieving top posts. diff --git a/projects/packages/stats/composer.json b/projects/packages/stats/composer.json index 72224163e738f..eb4fbecd8f20c 100644 --- a/projects/packages/stats/composer.json +++ b/projects/packages/stats/composer.json @@ -51,7 +51,7 @@ "link-template": "https://github.com/Automattic/jetpack-stats/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.7.x-dev" + "dev-trunk": "0.8.x-dev" }, "textdomain": "jetpack-stats" }, diff --git a/projects/packages/stats/src/class-wpcom-stats.php b/projects/packages/stats/src/class-wpcom-stats.php index b3f92c2e51ef4..cb155d7dbe109 100644 --- a/projects/packages/stats/src/class-wpcom-stats.php +++ b/projects/packages/stats/src/class-wpcom-stats.php @@ -80,11 +80,17 @@ public function get_stats_summary( $args = array() ) { * * @link https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/top-posts/ * @param array $args Optional query parameters. + * @param bool $override_cache Optional override cache. * @return array|WP_Error */ - public function get_top_posts( $args = array() ) { + public function get_top_posts( $args = array(), $override_cache = false ) { $this->resource = 'top-posts'; + // Needed for the Top Posts block, so users can preview changes instantly. + if ( $override_cache ) { + return $this->fetch_remote_stats( $this->build_endpoint(), $args ); + } + return $this->fetch_stats( $args ); } diff --git a/projects/packages/sync/CHANGELOG.md b/projects/packages/sync/CHANGELOG.md index 37a18abdd7d38..b385d8eaf9008 100644 --- a/projects/packages/sync/CHANGELOG.md +++ b/projects/packages/sync/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.1.2] - 2023-12-06 +### Changed +- Update dependencies. + ## [2.1.1] - 2023-12-03 ### Changed - Internal updates. @@ -987,6 +991,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Packages: Move sync to a classmapped package +[2.1.2]: https://github.com/Automattic/jetpack-sync/compare/v2.1.1...v2.1.2 [2.1.1]: https://github.com/Automattic/jetpack-sync/compare/v2.1.0...v2.1.1 [2.1.0]: https://github.com/Automattic/jetpack-sync/compare/v2.0.2...v2.1.0 [2.0.2]: https://github.com/Automattic/jetpack-sync/compare/v2.0.1...v2.0.2 diff --git a/projects/packages/sync/src/class-package-version.php b/projects/packages/sync/src/class-package-version.php index c6e23371818e8..4dc56b331a1fd 100644 --- a/projects/packages/sync/src/class-package-version.php +++ b/projects/packages/sync/src/class-package-version.php @@ -12,7 +12,7 @@ */ class Package_Version { - const PACKAGE_VERSION = '2.1.1'; + const PACKAGE_VERSION = '2.1.2'; const PACKAGE_SLUG = 'sync'; diff --git a/projects/packages/videopress/CHANGELOG.md b/projects/packages/videopress/CHANGELOG.md index d6267968ea24f..a928e23ed221b 100644 --- a/projects/packages/videopress/CHANGELOG.md +++ b/projects/packages/videopress/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.21.4] - 2023-12-06 +### Changed +- Updated package dependencies. [#34416] + ## [0.21.3] - 2023-12-03 ### Changed - Updated package dependencies. [#34411] [#34427] @@ -1202,6 +1206,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Created empty package [#24952] +[0.21.4]: https://github.com/Automattic/jetpack-videopress/compare/v0.21.3...v0.21.4 [0.21.3]: https://github.com/Automattic/jetpack-videopress/compare/v0.21.2...v0.21.3 [0.21.2]: https://github.com/Automattic/jetpack-videopress/compare/v0.21.1...v0.21.2 [0.21.1]: https://github.com/Automattic/jetpack-videopress/compare/v0.21.0...v0.21.1 diff --git a/projects/packages/videopress/changelog/renovate-babel-monorepo b/projects/packages/videopress/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/packages/videopress/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/packages/videopress/package.json b/projects/packages/videopress/package.json index 657d023874b3b..fc6bc8d11e063 100644 --- a/projects/packages/videopress/package.json +++ b/projects/packages/videopress/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@automattic/jetpack-videopress", - "version": "0.21.4-alpha", + "version": "0.21.4", "description": "VideoPress package", "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/videopress/#readme", "bugs": { diff --git a/projects/packages/videopress/src/class-package-version.php b/projects/packages/videopress/src/class-package-version.php index 96ee0fcc6d440..5611da28eabd4 100644 --- a/projects/packages/videopress/src/class-package-version.php +++ b/projects/packages/videopress/src/class-package-version.php @@ -11,7 +11,7 @@ * The Package_Version class. */ class Package_Version { - const PACKAGE_VERSION = '0.21.4-alpha'; + const PACKAGE_VERSION = '0.21.4'; const PACKAGE_SLUG = 'videopress'; diff --git a/projects/plugins/social/changelog/add-activitylog-menu b/projects/plugins/backup/changelog/add-send-secret-url-is-ip similarity index 100% rename from projects/plugins/social/changelog/add-activitylog-menu rename to projects/plugins/backup/changelog/add-send-secret-url-is-ip diff --git a/projects/plugins/social/changelog/add-declare-php-version-for-all-composer-packages b/projects/plugins/backup/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/add-declare-php-version-for-all-composer-packages rename to projects/plugins/backup/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/backup/composer.lock b/projects/plugins/backup/composer.lock index 272ba0870568e..98e282e7dba82 100644 --- a/projects/plugins/backup/composer.lock +++ b/projects/plugins/backup/composer.lock @@ -124,7 +124,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "335ee318534ab58327f45abc5da1748a76dd531d" + "reference": "1f713fb83a98dab43f325fe71331d40f9ca47334" }, "require": { "automattic/jetpack-constants": "@dev", @@ -133,7 +133,7 @@ "require-dev": { "automattic/jetpack-changelogger": "@dev", "brain/monkey": "2.6.1", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -615,7 +615,7 @@ "dist": { "type": "path", "url": "../../packages/identity-crisis", - "reference": "39eb9595c27599391f98ef53ead52300ccfe2901" + "reference": "e7f53dc4d861086ba733fc32571824c19350b9ca" }, "require": { "automattic/jetpack-assets": "@dev", @@ -645,7 +645,7 @@ "link-template": "https://github.com/Automattic/jetpack-identity-crisis/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.13.x-dev" + "dev-trunk": "0.14.x-dev" } }, "autoload": { @@ -1417,7 +1417,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -1425,7 +1425,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/social/changelog/add-jetpack-manage-menu-item b/projects/plugins/beta/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/add-jetpack-manage-menu-item rename to projects/plugins/beta/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/beta/composer.lock b/projects/plugins/beta/composer.lock index 2bc6dd69fc028..4c4e938c5fb16 100644 --- a/projects/plugins/beta/composer.lock +++ b/projects/plugins/beta/composer.lock @@ -270,7 +270,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -278,7 +278,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/boost/app/assets/src/js/lib/stores/minify.ts b/projects/plugins/boost/app/assets/src/js/lib/stores/minify.ts index afeb043a3d51b..e480a97f34b01 100644 --- a/projects/plugins/boost/app/assets/src/js/lib/stores/minify.ts +++ b/projects/plugins/boost/app/assets/src/js/lib/stores/minify.ts @@ -14,9 +14,7 @@ export interface Props { } export const useMetaQuery = ( key: MinifyMetaKeys ) => { - const { useQuery, useMutation } = useDataSync( 'jetpack_boost_ds', key, z.array( z.string() ) ); - const { data } = useQuery(); - const { mutate } = useMutation(); + const [ { data }, { mutate } ] = useDataSync( 'jetpack_boost_ds', key, z.array( z.string() ) ); function updateValues( text: string ) { mutate( text.split( ',' ).map( item => item.trim() ) ); @@ -26,14 +24,13 @@ export const useMetaQuery = ( key: MinifyMetaKeys ) => { }; export const useConfig = () => { - const { useQuery } = useDataSync( + const [ { data } ] = useDataSync( 'jetpack_boost_ds', 'config', z.object( { plugin_dir_url: z.string().url(), } ) ); - const { data } = useQuery(); return data; }; diff --git a/projects/plugins/boost/changelog/boost-react-datasync-improvements b/projects/plugins/boost/changelog/boost-react-datasync-improvements new file mode 100644 index 0000000000000..d09392e9d439a --- /dev/null +++ b/projects/plugins/boost/changelog/boost-react-datasync-improvements @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Jetpack Boost: Use the new datasync interface diff --git a/projects/plugins/social/changelog/add-my-jetpack-plans-dependency b/projects/plugins/boost/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/add-my-jetpack-plans-dependency rename to projects/plugins/boost/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/boost/composer.lock b/projects/plugins/boost/composer.lock index 89cf0b9d74975..a54a3c6708d92 100644 --- a/projects/plugins/boost/composer.lock +++ b/projects/plugins/boost/composer.lock @@ -124,7 +124,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "335ee318534ab58327f45abc5da1748a76dd531d" + "reference": "1f713fb83a98dab43f325fe71331d40f9ca47334" }, "require": { "automattic/jetpack-constants": "@dev", @@ -133,7 +133,7 @@ "require-dev": { "automattic/jetpack-changelogger": "@dev", "brain/monkey": "2.6.1", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -1640,7 +1640,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -1648,7 +1648,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/social/changelog/add-my-jetpack-stats-section b/projects/plugins/crm/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/add-my-jetpack-stats-section rename to projects/plugins/crm/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/crm/composer.lock b/projects/plugins/crm/composer.lock index 9ab2c959a7eb4..d3d94cbe2938e 100644 --- a/projects/plugins/crm/composer.lock +++ b/projects/plugins/crm/composer.lock @@ -12,7 +12,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "335ee318534ab58327f45abc5da1748a76dd531d" + "reference": "1f713fb83a98dab43f325fe71331d40f9ca47334" }, "require": { "automattic/jetpack-constants": "@dev", @@ -21,7 +21,7 @@ "require-dev": { "automattic/jetpack-changelogger": "@dev", "brain/monkey": "2.6.1", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -576,7 +576,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -584,7 +584,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/debug-helper/changelog/add-xmlrpc-logger-jetpack-debug-helper b/projects/plugins/debug-helper/changelog/add-xmlrpc-logger-jetpack-debug-helper new file mode 100644 index 0000000000000..d9a4578420c3a --- /dev/null +++ b/projects/plugins/debug-helper/changelog/add-xmlrpc-logger-jetpack-debug-helper @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Added XML-RPC logger module diff --git a/projects/plugins/social/changelog/add-permanent-wpcom-id b/projects/plugins/debug-helper/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/add-permanent-wpcom-id rename to projects/plugins/debug-helper/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/debug-helper/changelog/update-use-error_log_not_l_xmlrpc_logger b/projects/plugins/debug-helper/changelog/update-use-error_log_not_l_xmlrpc_logger new file mode 100644 index 0000000000000..2fbb769ca58da --- /dev/null +++ b/projects/plugins/debug-helper/changelog/update-use-error_log_not_l_xmlrpc_logger @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Resort to error_log if l() is not available diff --git a/projects/plugins/debug-helper/composer.json b/projects/plugins/debug-helper/composer.json index a9293cc9aaf66..939361e7cac13 100644 --- a/projects/plugins/debug-helper/composer.json +++ b/projects/plugins/debug-helper/composer.json @@ -23,6 +23,8 @@ "version-constants": { "JETPACK_DEBUG_HELPER_VERSION": "plugin.php" }, + "release-branch-prefix": "debug-helper", + "beta-plugin-slug": "jetpack-debug-helper", "changelogger": { "link-template": "https://github.com/Automattic/jetpack-debug-helper/compare/v${old}...v${new}" } diff --git a/projects/plugins/debug-helper/composer.lock b/projects/plugins/debug-helper/composer.lock index df5d5edf688e6..c5b08aeb7dd89 100644 --- a/projects/plugins/debug-helper/composer.lock +++ b/projects/plugins/debug-helper/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1b93c13c400642c5a702ebc5ab0805c7", + "content-hash": "90508ab73efdc01ec5346540c36554d7", "packages": [], "packages-dev": [ { @@ -13,7 +13,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -21,7 +21,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/debug-helper/modules/class-xmlrpc-logger.php b/projects/plugins/debug-helper/modules/class-xmlrpc-logger.php new file mode 100644 index 0000000000000..ac15947e0cc4a --- /dev/null +++ b/projects/plugins/debug-helper/modules/class-xmlrpc-logger.php @@ -0,0 +1,275 @@ +settings = $this->get_stored_settings(); + // Hook into the WordPress initialization process to log XML-RPC requests. + add_action( 'init', array( $this, 'log_xmlrpc_requests_on_init' ) ); + // Hook into the WordPress admin menu to register the XML-RPC logger submenu page. + add_action( 'admin_menu', array( $this, 'register_submenu_page' ), 1000 ); + } + + /** + * Logs XML-RPC requests. + * Checks if the current request is a POST to xmlrpc.php and logs. + */ + public function log_xmlrpc_requests_on_init() { + if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_SERVER['SCRIPT_FILENAME'] ) && basename( esc_url_raw( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) ) ) === 'xmlrpc.php' + && $this->settings['log_incoming_xmlrpc_requests'] + ) { + $this->log_xmlrpc_request(); + } + } + + /** + * Registers the XML-RPC logger submenu page. + */ + public function register_submenu_page() { + add_submenu_page( + 'jetpack-debug-tools', + 'XML-RPC Logger', + 'XML-RPC Logger', + 'manage_options', + 'jetpack_xmlrpc_logger', + array( $this, 'render_submenu_page' ) + ); + } + + /** + * Retrieves the stored XML-RPC logger settings. + * + * @return array The stored XML-RPC logger settings. + */ + public function get_stored_settings() { + $defaults = array( + 'log_incoming_xmlrpc_requests' => true, + 'log_xmlrpc_requests_as_json' => false, + ); + $settings = get_option( 'jetpack_xmlrpc_logger_settings', $defaults ); + return wp_parse_args( $settings, $defaults ); + } + + /** + * Saves the XML-RPC logger settings. + */ + public function maybe_handle_submit() { + if ( isset( $_POST['save_xmlrpc_logger'] ) ) { + check_admin_referer( 'xmlrpc_logger_nonce' ); + } else { + return; + } + $this->settings = $this->get_stored_settings(); + $this->settings['log_incoming_xmlrpc_requests'] = isset( $_POST['log_incoming_xmlrpc_requests'] ); + $this->settings['log_xmlrpc_requests_as_json'] = isset( $_POST['log_xmlrpc_requests_as_json'] ); + return update_option( 'jetpack_xmlrpc_logger_settings', $this->settings ); + } + + /** + * Renders the XML-RPC logger settings page. + */ + public function render_submenu_page() { + $this->maybe_handle_submit(); + + $log_incoming_xmlrpc_requests_checked = $this->settings['log_incoming_xmlrpc_requests'] ? 'checked="checked"' : ''; + $log_xmlrpc_requests_as_json_checked = $this->settings['log_xmlrpc_requests_as_json'] ? 'checked="checked"' : ''; + ?> +

XML-RPC Logger

+

This module helps you log all incoming XML-RPC requests.

+

All instances of logging are stored in debug.log. Nothing done here will alter or expose sensitive data.

+
+ +

Current XML-RPC options being used by the module:

+
+ + + +
+ + + + + + + + + + + +
+ Log incoming XML-RPC requests + +
+ +
+
+ Log as JSON + +
+ +
+
+ +
+ +
+ +
+
+ log( 'XML-RPC Request: Empty payload' ); + return; + } + libxml_use_internal_errors( true ); + $xml = simplexml_load_string( $xml_string ); + + if ( $xml ) { + // Extract the method name + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $method_name = $xml->methodName ? (string) $xml->methodName : 'Unknown Method'; + $formatted = $this->settings['log_xmlrpc_requests_as_json'] ? $this->convert_xml_rpc_to_json( $xml ) : $this->pretty_print_xml( $xml_string ); + + // Format and log the request + if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { + $log_message = "XML-RPC Request - Method: $method_name\n"; + $log_message .= "Payload:\n" . $formatted . "\n"; + $this->log( $log_message ); + } + } else { + $this->log( 'XML-RPC Request: Invalid XML - ' . libxml_get_errors()[0]->message ); + libxml_clear_errors(); + return; + } + } + + /** + * Pretty prints the XML. + * + * @param string $xml The XML to pretty print. + * @return string The pretty printed XML. + */ + public function pretty_print_xml( $xml ) { + $dom = new \DOMDocument(); + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $dom->preserveWhiteSpace = false; + $dom->loadXML( $xml ); + + // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $dom->formatOutput = true; + return $dom->saveXML(); + } + + /** + * Converts the XML-RPC request to JSON. + * + * @param \SimpleXMLElement $xml The XML to convert. + * @return string The JSON string. + */ + public function convert_xml_rpc_to_json( $xml ) { + // Convert SimpleXML object to an array + // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode, WordPress.WP.AlternativeFunctions.json_decode_json_decode + $array = json_decode( json_encode( (array) $xml ), true ); + + // Recursively clean up the array from empty arrays and objects + $array = $this->recursive_array_clean( $array ); + + // Convert the array to a JSON string + // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode + return json_encode( $array, JSON_PRETTY_PRINT ); + } + + /** + * Recursively cleans up an array from empty arrays and objects. + * + * @param array $array The array to clean up. + * @return array The cleaned up array. + */ + public function recursive_array_clean( $array ) { + foreach ( $array as $key => $value ) { + if ( is_array( $value ) ) { + $array[ $key ] = $this->recursive_array_clean( $array[ $key ] ); + } + + // Remove empty arrays and objects + if ( empty( $array[ $key ] ) ) { + unset( $array[ $key ] ); + } + } + + return $array; + } + + /** + * Load the class. + */ + public static function register_xmlrpc_logger() { + new self(); + } +} + +add_action( 'plugins_loaded', array( XMLRPC_Logger::class, 'register_xmlrpc_logger' ), 1000 ); diff --git a/projects/plugins/debug-helper/plugin.php b/projects/plugins/debug-helper/plugin.php index edb9c4b0e284c..de421c3e2dd72 100644 --- a/projects/plugins/debug-helper/plugin.php +++ b/projects/plugins/debug-helper/plugin.php @@ -104,6 +104,11 @@ 'name' => 'WPCOM API Request Tracker', 'description' => 'Displays the number of requests to WPCOM API endpoints for the current page request.', ), + 'xmlrpc-logger' => array( + 'file' => 'class-xmlrpc-logger.php', + 'name' => 'XMLRPC Logger', + 'description' => 'Logs incoming XMLRPC requests into the debug.log file.', + ), ); require_once __DIR__ . '/class-admin.php'; diff --git a/projects/plugins/social/changelog/add-private-network-connection b/projects/plugins/inspect/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/add-private-network-connection rename to projects/plugins/inspect/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/inspect/composer.lock b/projects/plugins/inspect/composer.lock index 008f23b2c0d2d..880c989c3681b 100644 --- a/projects/plugins/inspect/composer.lock +++ b/projects/plugins/inspect/composer.lock @@ -124,7 +124,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "335ee318534ab58327f45abc5da1748a76dd531d" + "reference": "1f713fb83a98dab43f325fe71331d40f9ca47334" }, "require": { "automattic/jetpack-constants": "@dev", @@ -133,7 +133,7 @@ "require-dev": { "automattic/jetpack-changelogger": "@dev", "brain/monkey": "2.6.1", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -637,7 +637,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -645,7 +645,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/jetpack/_inc/blogging-prompts.php b/projects/plugins/jetpack/_inc/blogging-prompts.php index 47dd5edf46cfe..f00541e6ed8b0 100644 --- a/projects/plugins/jetpack/_inc/blogging-prompts.php +++ b/projects/plugins/jetpack/_inc/blogging-prompts.php @@ -48,6 +48,9 @@ function jetpack_setup_blogging_prompt_response( $post_id ) { if ( $prompt ) { update_post_meta( $post_id, '_jetpack_blogging_prompt_key', $prompt_id ); wp_add_post_tags( $post_id, array( 'dailyprompt', "dailyprompt-$prompt_id" ) ); + if ( array_key_exists( 'bloganuary_id', $prompt ) ) { + wp_add_post_tags( $post_id, array( 'bloganuary', $prompt['bloganuary_id'] ) ); + } } } @@ -118,6 +121,8 @@ function jetpack_get_blogging_prompt_by_id( $prompt_id ) { $request = new WP_REST_Request( 'GET', $route ); $request->set_param( '_locale', $locale ); + $request->set_param( 'force_year', gmdate( 'Y' ) ); + $response = rest_do_request( $request ); if ( $response->is_error() || WP_Http::OK !== $response->get_status() ) { diff --git a/projects/plugins/jetpack/_inc/client/at-a-glance/style.scss b/projects/plugins/jetpack/_inc/client/at-a-glance/style.scss index 28575a29684dc..c8bf954b62481 100644 --- a/projects/plugins/jetpack/_inc/client/at-a-glance/style.scss +++ b/projects/plugins/jetpack/_inc/client/at-a-glance/style.scss @@ -39,6 +39,10 @@ padding: 0; } } + + .jp-dash-item__content .jp-support-info { + top: auto; + } } .jp-dash-section-header.jp-dash-section-header-stats { @@ -672,7 +676,7 @@ a.jp-dash-item__manage-in-wpcom, display: flex; flex-direction: row; } - + .dash-backup-undo__activity-log-user-meta-avatar { display: flex; margin-right: 8px; diff --git a/projects/plugins/jetpack/_inc/client/at-a-glance/videopress.jsx b/projects/plugins/jetpack/_inc/client/at-a-glance/videopress.jsx index 5a3a6a153c313..ad197283e3325 100644 --- a/projects/plugins/jetpack/_inc/client/at-a-glance/videopress.jsx +++ b/projects/plugins/jetpack/_inc/client/at-a-glance/videopress.jsx @@ -75,7 +75,7 @@ class DashVideoPress extends Component { /* dummy arg to avoid bad minification */ 0 ); - if ( this.props.getOptionValue( 'videopress' ) && hasConnectedOwner ) { + if ( this.props.getOptionValue( 'videopress' ) && hasConnectedOwner && ! isOffline ) { return ( $posts_to_obtain_count, + 'summarize' => true, + 'num' => $period !== 'all-time' ? $period : $all_time_days, + 'period' => 'day', + ); + + $data = ( new WPCOM_Stats() )->get_top_posts( $query_args, $override_cache ); + + if ( is_wp_error( $data ) ) { + $data = array( 'summary' => array( 'postviews' => array() ) ); + } + + $posts_retrieved = count( $data['summary']['postviews'] ); + + // Fallback to random posts if user does not have enough top content. + if ( $posts_retrieved < $posts_to_obtain_count ) { + $args = array( + 'numberposts' => $posts_to_obtain_count - $posts_retrieved, + 'exclude' => array_column( $data['summary']['postviews'], 'id' ), + 'orderby' => 'rand', + 'post_status' => 'publish', + ); + + $random_posts = get_posts( $args ); + + foreach ( $random_posts as $post ) { + $random_posts_data = array( + 'id' => $post->ID, + 'href' => get_permalink( $post->ID ), + 'date' => $post->post_date, + 'title' => $post->post_title, + 'type' => 'post', + 'public' => true, + ); + + $data['summary']['postviews'][] = $random_posts_data; + } + + $data['summary']['postviews'] = array_slice( $data['summary']['postviews'], 0, 10 ); + } + + $top_posts = array(); + + foreach ( $data['summary']['postviews'] as $post ) { + $post_id = $post['id']; + $thumbnail = get_the_post_thumbnail_url( $post_id ); + + if ( ! $thumbnail ) { + $post_images = get_attached_media( 'image', $post_id ); + $post_image = reset( $post_images ); + if ( $post_image ) { + $thumbnail = wp_get_attachment_url( $post_image->ID ); + } + } + + if ( $post['public'] ) { + $top_posts[] = array( + 'id' => $post_id, + 'author' => get_the_author_meta( 'display_name', get_post_field( 'post_author', $post_id ) ), + 'context' => get_the_category( $post_id ) ? get_the_category( $post_id ) : get_the_tags( $post_id ), + 'href' => $post['href'], + 'date' => get_the_date( '', $post_id ), + 'title' => $post['title'], + 'type' => $post['type'], + 'public' => $post['public'], + 'views' => isset( $post['views'] ) ? $post['views'] : 0, + 'thumbnail' => $thumbnail, + ); + } + } + + // This applies for rendering the block front-end, but not for editing it. + if ( $is_rendering_block ) { + $acceptable_types = explode( ',', $types ); + + $top_posts = array_filter( + $top_posts, + function ( $item ) use ( $acceptable_types ) { + return in_array( $item['type'], $acceptable_types, true ); + } + ); + + $top_posts = array_slice( $top_posts, 0, $items_count ); + } + + return $top_posts; + } +} diff --git a/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-top-posts.php b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-top-posts.php new file mode 100644 index 0000000000000..665efa94b39a6 --- /dev/null +++ b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-top-posts.php @@ -0,0 +1,116 @@ + WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_post_types' ), + 'permission_callback' => function () { + return current_user_can( 'edit_posts' ); + }, + ), + ) + ); + + // Number of posts and selected post types are not needed in the Editor. + // This is to minimise requests when it can already be handled by the block. + register_rest_route( + 'wpcom/v2', + '/top-posts', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_top_posts' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'period' => array( + 'description' => __( 'Timeframe for stats.', 'jetpack' ), + 'type' => array( 'string', 'integer' ), + 'required' => true, + 'validate_callback' => function ( $param ) { + return is_numeric( $param ) || is_string( $param ); + }, + ), + 'number' => array( + 'description' => __( 'Number of posts to display.', 'jetpack' ), + 'type' => 'integer', + 'required' => false, + 'validate_callback' => function ( $param ) { + return is_numeric( $param ); + }, + ), + 'types' => array( + 'description' => __( 'Types of content to include.', 'jetpack' ), + 'type' => 'string', + 'required' => false, + 'validate_callback' => function ( $param ) { + return is_string( $param ); + }, + ), + ), + ), + ) + ); + } + + /** + * Get the site's post types. + * + * @return array Site's post types. + */ + public function get_post_types() { + $post_types = array_values( get_post_types( array( 'public' => true ) ) ); + $post_types_array = array(); + + foreach ( $post_types as $type ) { + $post_types_array[] = array( + 'label' => get_post_type_object( $type )->labels->name, + 'id' => $type, + ); + } + + return $post_types_array; + } + + /** + * Get the site's top content. + * + * @param \WP_REST_Request $request request object. + * + * @return array Data on top posts. + */ + public function get_top_posts( $request ) { + $period = $request->get_param( 'period' ); + $number = $request->get_param( 'number' ); + $types = $request->get_param( 'types' ); + return Jetpack_Top_Posts_Helper::get_top_posts( $period, $number, $types ); + } +} + +wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Top_Posts' ); diff --git a/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php index f66d012322b7c..64b015e8fcdeb 100644 --- a/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php +++ b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v3-endpoint-blogging-prompts.php @@ -128,6 +128,10 @@ public function get_item( $request ) { return $this->proxy_request_to_wpcom( $request, $request->get_param( 'id' ) ); } + if ( $request->get_param( 'force_year' ) ) { + $this->force_year = $request->get_param( 'force_year' ); + } + switch_to_blog( self::TEMPLATE_BLOG_ID ); $item = parent::get_item( $request ); restore_current_blog(); @@ -533,11 +537,11 @@ public function proxy_request_to_wpcom( $request, $path = '' ) { } $response_status = wp_remote_retrieve_response_code( $response ); - $response_body = json_decode( wp_remote_retrieve_body( $response ) ); + $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $response_status >= 400 ) { - $code = isset( $response_body->code ) ? $response_body->code : 'unknown_error'; - $message = isset( $response_body->message ) ? $response_body->message : __( 'An unknown error occurred.', 'jetpack' ); + $code = isset( $response_body['code'] ) ? $response_body['code'] : 'unknown_error'; + $message = isset( $response_body['message'] ) ? $response_body['message'] : __( 'An unknown error occurred.', 'jetpack' ); return new WP_Error( $code, $message, array( 'status' => $response_status ) ); } diff --git a/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/trait-wpcom-rest-api-proxy-request-trait.php b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/trait-wpcom-rest-api-proxy-request-trait.php index b73a0d4b6363c..c7ecc011640b4 100644 --- a/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/trait-wpcom-rest-api-proxy-request-trait.php +++ b/projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/trait-wpcom-rest-api-proxy-request-trait.php @@ -54,11 +54,11 @@ public function proxy_request_to_wpcom_as_user( $request, $path = '' ) { } $response_status = wp_remote_retrieve_response_code( $response ); - $response_body = json_decode( wp_remote_retrieve_body( $response ) ); + $response_body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $response_status >= 400 ) { - $code = isset( $response_body->code ) ? $response_body->code : 'unknown_error'; - $message = isset( $response_body->message ) ? $response_body->message : __( 'An unknown error occurred.', 'jetpack' ); + $code = isset( $response_body['code'] ) ? $response_body['code'] : 'unknown_error'; + $message = isset( $response_body['message'] ) ? $response_body['message'] : __( 'An unknown error occurred.', 'jetpack' ); return new WP_Error( $code, $message, array( 'status' => $response_status ) ); } diff --git a/projects/plugins/jetpack/changelog/add-bloganuary-add-tags b/projects/plugins/jetpack/changelog/add-bloganuary-add-tags new file mode 100644 index 0000000000000..b7f0fe1c26f95 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-bloganuary-add-tags @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +add force_year param to individual get for blogging prompt. Add bloganuary tags for prompts in january. diff --git a/projects/plugins/social/changelog/update-my-jetpack-ctas b/projects/plugins/jetpack/changelog/add-contact-form-onblur-validation similarity index 79% rename from projects/plugins/social/changelog/update-my-jetpack-ctas rename to projects/plugins/jetpack/changelog/add-contact-form-onblur-validation index 9aa70e3ec1f75..a1c1831fa1ef7 100644 --- a/projects/plugins/social/changelog/update-my-jetpack-ctas +++ b/projects/plugins/jetpack/changelog/add-contact-form-onblur-validation @@ -1,5 +1,5 @@ Significance: patch -Type: changed +Type: other Comment: Updated composer.lock. diff --git a/projects/plugins/jetpack/changelog/add-iframe-to-Like-block-render b/projects/plugins/jetpack/changelog/add-iframe-to-Like-block-render new file mode 100644 index 0000000000000..18ea7c9290c76 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-iframe-to-Like-block-render @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Like: Add iframe to the new Like block (beta) diff --git a/projects/plugins/social/changelog/update-php-requirements#2 b/projects/plugins/jetpack/changelog/add-send-secret-url-is-ip similarity index 79% rename from projects/plugins/social/changelog/update-php-requirements#2 rename to projects/plugins/jetpack/changelog/add-send-secret-url-is-ip index 9aa70e3ec1f75..a1c1831fa1ef7 100644 --- a/projects/plugins/social/changelog/update-php-requirements#2 +++ b/projects/plugins/jetpack/changelog/add-send-secret-url-is-ip @@ -1,5 +1,5 @@ Significance: patch -Type: changed +Type: other Comment: Updated composer.lock. diff --git a/projects/plugins/jetpack/changelog/add-top-posts-pages-block b/projects/plugins/jetpack/changelog/add-top-posts-pages-block new file mode 100644 index 0000000000000..b23f1a5821eda --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-top-posts-pages-block @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Gutenberg: Add Top Posts & Pages block. diff --git a/projects/plugins/jetpack/changelog/fix-css-changes-like-avatar b/projects/plugins/jetpack/changelog/fix-css-changes-like-avatar new file mode 100644 index 0000000000000..686c5adb1b56c --- /dev/null +++ b/projects/plugins/jetpack/changelog/fix-css-changes-like-avatar @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Removed like avatar border inside the popup diff --git a/projects/plugins/jetpack/changelog/fix-launchpad-is-saving-site b/projects/plugins/jetpack/changelog/fix-launchpad-is-saving-site new file mode 100644 index 0000000000000..bd012597d58e3 --- /dev/null +++ b/projects/plugins/jetpack/changelog/fix-launchpad-is-saving-site @@ -0,0 +1,4 @@ +Significance: patch +Type: bugfix + +Launchpad: Fix the Save modal doesn't show after saving changes in the editor diff --git a/projects/plugins/jetpack/changelog/fix-proxy-return-arrays b/projects/plugins/jetpack/changelog/fix-proxy-return-arrays new file mode 100644 index 0000000000000..8149d69ce61ef --- /dev/null +++ b/projects/plugins/jetpack/changelog/fix-proxy-return-arrays @@ -0,0 +1,5 @@ +Significance: patch +Type: other +Comment: technically just refactoring as there is no change to behaviour of any APIs + + diff --git a/projects/plugins/jetpack/changelog/fix-videopress-card-offline-mode b/projects/plugins/jetpack/changelog/fix-videopress-card-offline-mode new file mode 100644 index 0000000000000..ffd42b76a5b82 --- /dev/null +++ b/projects/plugins/jetpack/changelog/fix-videopress-card-offline-mode @@ -0,0 +1,4 @@ +Significance: patch +Type: bugfix + +Dashboard: disable VideoPress card in offline mode. diff --git a/projects/plugins/jetpack/changelog/refactor-wpcom-token-subscription-service-into-jetpack-token-subscription-service b/projects/plugins/jetpack/changelog/refactor-wpcom-token-subscription-service-into-jetpack-token-subscription-service new file mode 100644 index 0000000000000..6be66567bf2eb --- /dev/null +++ b/projects/plugins/jetpack/changelog/refactor-wpcom-token-subscription-service-into-jetpack-token-subscription-service @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Consolidate WPCOM/Jetpack Token Subscription Service classes. diff --git a/projects/plugins/social/changelog/update-jetpack-no-disconnect-on-uninstall b/projects/plugins/jetpack/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 79% rename from projects/plugins/social/changelog/update-jetpack-no-disconnect-on-uninstall rename to projects/plugins/jetpack/changelog/renovate-wikimedia-testing-access-wrapper-3.x index 9aa70e3ec1f75..a1c1831fa1ef7 100644 --- a/projects/plugins/social/changelog/update-jetpack-no-disconnect-on-uninstall +++ b/projects/plugins/jetpack/changelog/renovate-wikimedia-testing-access-wrapper-3.x @@ -1,5 +1,5 @@ Significance: patch -Type: changed +Type: other Comment: Updated composer.lock. diff --git a/projects/plugins/jetpack/changelog/update-blogroll-beta-removal b/projects/plugins/jetpack/changelog/update-blogroll-beta-removal new file mode 100644 index 0000000000000..0aecab7c5dfa7 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-blogroll-beta-removal @@ -0,0 +1,4 @@ +Significance: minor +Type: other + +Remove Blogroll block "beta" text diff --git a/projects/plugins/jetpack/changelog/update-jetpack-ai-fix-usage-panel-spacing b/projects/plugins/jetpack/changelog/update-jetpack-ai-fix-usage-panel-spacing new file mode 100644 index 0000000000000..e083de1727690 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-jetpack-ai-fix-usage-panel-spacing @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Jetpack AI: fix element spacing on usage panel when it's on the block inspector. diff --git a/projects/plugins/jetpack/changelog/update-jetpack-ai-improve-event-tracking b/projects/plugins/jetpack/changelog/update-jetpack-ai-improve-event-tracking new file mode 100644 index 0000000000000..219fa9d7fe76e --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-jetpack-ai-improve-event-tracking @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Jetpack AI: Add event tracking on the usage panel button. diff --git a/projects/plugins/jetpack/changelog/update-top-posts-endpoint b/projects/plugins/jetpack/changelog/update-top-posts-endpoint new file mode 100644 index 0000000000000..3be4be7075a60 --- /dev/null +++ b/projects/plugins/jetpack/changelog/update-top-posts-endpoint @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Top Posts and Pages block: refactor endpoint to use helper. diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js index 21b288d0ef02a..4b1bad45e1d82 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/edit.js @@ -28,6 +28,7 @@ import { useEffect, useRef } from 'react'; * Internal dependencies */ import UsagePanel from '../../plugins/ai-assistant-plugin/components/usage-panel'; +import { USAGE_PANEL_PLACEMENT_BLOCK_SETTINGS_SIDEBAR } from '../../plugins/ai-assistant-plugin/components/usage-panel/types'; import ConnectPrompt from './components/connect-prompt'; import ImageWithSelect from './components/image-with-select'; import { promptTemplates } from './components/prompt-templates-control'; @@ -450,7 +451,7 @@ export default function AIAssistantEdit( { attributes, setAttributes, clientId, - + diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/global.d.ts b/projects/plugins/jetpack/extensions/blocks/ai-assistant/global.d.ts index f7f5c09cf71a6..189228fcf9fcd 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/global.d.ts +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/global.d.ts @@ -5,5 +5,10 @@ interface Window { available_blocks: { 'jetpack/ai-assistant-support': boolean; }; + tracksUserData: { + userid: number; + username: string; + }; + wpcomBlogId: string; }; } diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/hooks/use-analytics/index.ts b/projects/plugins/jetpack/extensions/blocks/ai-assistant/hooks/use-analytics/index.ts new file mode 100644 index 0000000000000..153cf2d20687d --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/hooks/use-analytics/index.ts @@ -0,0 +1,25 @@ +import jetpackAnalytics from '@automattic/jetpack-analytics'; +import { useEffect } from '@wordpress/element'; + +// Get user data from the inial state +const tracksUserData = window?.Jetpack_Editor_Initial_State?.tracksUserData || null; +const blogId = parseInt( window?.Jetpack_Editor_Initial_State?.wpcomBlogId ) || 0; + +const useAnalytics = () => { + /** + * Initialize tracks with user data. + */ + useEffect( () => { + if ( tracksUserData ) { + jetpackAnalytics.initialize( + tracksUserData?.userid, + tracksUserData?.username, + blogId ? { blog_id: blogId } : {} + ); + } + }, [] ); + + return jetpackAnalytics; +}; + +export default useAnalytics; diff --git a/projects/plugins/jetpack/extensions/blocks/blogroll/block.json b/projects/plugins/jetpack/extensions/blocks/blogroll/block.json index 035b48693f880..5da06983d991e 100644 --- a/projects/plugins/jetpack/extensions/blocks/blogroll/block.json +++ b/projects/plugins/jetpack/extensions/blocks/blogroll/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "jetpack/blogroll", - "title": "Blogroll (Beta)", + "title": "Blogroll", "description": "Share the sites you follow with your users.", "keywords": [], "version": "12.5.0", diff --git a/projects/plugins/jetpack/extensions/blocks/like/block.json b/projects/plugins/jetpack/extensions/blocks/like/block.json index f16a1a82f0f84..9a482775b0121 100644 --- a/projects/plugins/jetpack/extensions/blocks/like/block.json +++ b/projects/plugins/jetpack/extensions/blocks/like/block.json @@ -8,5 +8,6 @@ "version": "1.0.0", "textdomain": "jetpack", "category": "grow", - "icon": "" + "icon": "", + "usesContext": [ "postId" ] } diff --git a/projects/plugins/jetpack/extensions/blocks/like/like.php b/projects/plugins/jetpack/extensions/blocks/like/like.php index 569351a709be1..ebcbf596c1d5c 100644 --- a/projects/plugins/jetpack/extensions/blocks/like/like.php +++ b/projects/plugins/jetpack/extensions/blocks/like/like.php @@ -31,21 +31,72 @@ function register_block() { /** * Like block render function. * - * @param array $attr Array containing the Like block attributes. + * @param array $attr Array containing the Like block attributes. + * @param string $content String containing the Like block content. + * @param object $block Object containing the Like block data. * * @return string */ -function render_block( $attr ) { +function render_block( $attr, $content, $block ) { /* * Enqueue necessary scripts and styles. */ Jetpack_Gutenberg::load_assets_as_required( __DIR__ ); - $output = 'This is where the like button will go.'; + $html = ''; + + $uniqid = uniqid(); + $post_id = $block->context['postId']; + $title = esc_html__( 'Like or Reblog', 'jetpack' ); + + /** + * Enable an alternate Likes layout. + * + * @since 12.9 + * + * @module likes + * + * @param bool $new_layout Enable the new Likes layout. False by default. + */ + $new_layout = apply_filters( 'likes_new_layout', true ) ? '&n=1' : ''; + + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { + $blog_id = get_current_blog_id(); + $bloginfo = get_blog_details( (int) $blog_id ); + $domain = $bloginfo->domain; + $version = '20231201'; + $src = sprintf( '//widgets.wp.com/likes/index.html?ver=%1$d#blog_id=%2$d&post_id=%3$d&origin=%4$s&obj_id=%2$d-%3$d-%5$s%6$s', $version, $blog_id, $post_id, $domain, $uniqid, $new_layout ); + + // provide the mapped domain when needed + if ( isset( $_SERVER['HTTP_HOST'] ) && strpos( sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ), '.wordpress.com' ) === false ) { + $sanitized_host = filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ), FILTER_SANITIZE_URL ); + $src .= '&domain=' . rawurlencode( $sanitized_host ); + } + } else { + $blog_id = \Jetpack_Options::get_option( 'id' ); + $url = home_url(); + $url_parts = wp_parse_url( $url ); + $domain = $url_parts['host']; + $src = sprintf( 'https://widgets.wp.com/likes/#blog_id=%1$d&post_id=%2$d&origin=%3$s&obj_id=%1$d-%2$d-%4$s%5$s', $blog_id, $post_id, $domain, $uniqid, $new_layout ); + $headline = sprintf( + /** This filter is already documented in modules/sharedaddy/sharing-service.php */ + apply_filters( 'jetpack_sharing_headline_html', '

%s

', esc_html__( 'Like this:', 'jetpack' ), 'likes' ), + esc_html__( 'Like this:', 'jetpack' ) + ); + } + + $name = sprintf( 'like-post-frame-%1$d-%2$d-%3$s', $blog_id, $post_id, $uniqid ); + $wrapper = sprintf( 'like-post-wrapper-%1$d-%2$d-%3$s', $blog_id, $post_id, $uniqid ); + + $html = "'; return sprintf( '
%2$s
', esc_attr( Blocks::classes( Blocks::get_block_feature( __DIR__ ), $attr ) ), - $output + $html ); } diff --git a/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jetpack-token-subscription-service.php b/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jetpack-token-subscription-service.php index 2dc8cfb3a83ac..258dbf0fa07d2 100644 --- a/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jetpack-token-subscription-service.php +++ b/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-jetpack-token-subscription-service.php @@ -9,6 +9,7 @@ namespace Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service; use Automattic\Jetpack\Connection\Tokens; +use Automattic\Jetpack\Status\Host; /** * Class Jetpack_Token_Subscription_Service @@ -23,7 +24,7 @@ class Jetpack_Token_Subscription_Service extends Token_Subscription_Service { * @return bool Whether Jetpack_Options class exists. */ public static function available() { - return class_exists( '\Jetpack_Options' ); + return ( new Host() )->is_wpcom_simple() || class_exists( '\Jetpack_Options' ); } /** @@ -41,6 +42,10 @@ public function get_site_id() { * @return string The key. */ public function get_key() { + if ( ( new Host() )->is_wpcom_simple() ) { + // phpcs:ignore ImportDetection.Imports.RequireImports.Symbol + return defined( 'EARN_JWT_SIGNING_KEY' ) ? EARN_JWT_SIGNING_KEY : false; + } $token = ( new Tokens() )->get_access_token(); if ( ! isset( $token->secret ) ) { return false; diff --git a/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-online-subscription-service.php b/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-online-subscription-service.php index ab000a031ac5f..1ae9f57efd11c 100644 --- a/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-online-subscription-service.php +++ b/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-online-subscription-service.php @@ -12,7 +12,7 @@ * * @package Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service */ -class WPCOM_Online_Subscription_Service extends WPCOM_Token_Subscription_Service { +class WPCOM_Online_Subscription_Service extends Jetpack_Token_Subscription_Service { /** * Is available() diff --git a/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-token-subscription-service.php b/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-token-subscription-service.php deleted file mode 100644 index bb0c052b00b43..0000000000000 --- a/projects/plugins/jetpack/extensions/blocks/premium-content/_inc/subscription-service/class-wpcom-token-subscription-service.php +++ /dev/null @@ -1,55 +0,0 @@ -; + return ; } if ( ! isModuleActive || ! isEnabled ) { diff --git a/projects/plugins/jetpack/extensions/blocks/subscriptions/subscriptions.php b/projects/plugins/jetpack/extensions/blocks/subscriptions/subscriptions.php index f755c2798cdaf..5f72c64d3de7f 100644 --- a/projects/plugins/jetpack/extensions/blocks/subscriptions/subscriptions.php +++ b/projects/plugins/jetpack/extensions/blocks/subscriptions/subscriptions.php @@ -11,13 +11,13 @@ use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Jetpack_Token_Subscription_Service; use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Token_Subscription_Service; -use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\WPCOM_Token_Subscription_Service; use Automattic\Jetpack\Status; use Automattic\Jetpack\Status\Host; use Jetpack; use Jetpack_Gutenberg; use Jetpack_Memberships; use Jetpack_Subscriptions_Widget; +use function Automattic\Jetpack\Extensions\Premium_Content\subscription_service; require_once __DIR__ . '/constants.php'; @@ -807,7 +807,7 @@ function add_paywall( $the_content ) { } require_once JETPACK__PLUGIN_DIR . 'extensions/blocks/premium-content/_inc/subscription-service/include.php'; - $token_service = is_wpcom() ? new WPCOM_Token_Subscription_Service() : new Jetpack_Token_Subscription_Service(); + $token_service = subscription_service(); $token = $token_service->get_and_set_token_from_request(); $payload = $token_service->decode_token( $token ); $is_valid_token = ! empty( $payload ); diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/block.json b/projects/plugins/jetpack/extensions/blocks/top-posts/block.json new file mode 100644 index 0000000000000..4cb2c4037e939 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/block.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 1, + "name": "jetpack/top-posts", + "title": "Top Posts & Pages", + "description": "Display your most popular content.", + "keywords": [ "ranking", "views", "trending", "popular" ], + "version": "1.0", + "textdomain": "jetpack", + "category": "embed", + "icon": "", + "supports": { + "align": [ "wide", "full" ], + "html": false, + "multiple": true, + "reusable": true, + "color": { + "gradients": true, + "link": true + }, + "spacing": { + "margin": true, + "padding": true + }, + "typography": { + "__experimentalFontFamily": true, + "fontSize": true, + "lineHeight": true + } + }, + "attributes": { + "layout": { + "type": "string", + "default": "grid" + }, + "displayAuthor": { + "type": "boolean", + "default": true + }, + "displayDate": { + "type": "boolean", + "default": true + }, + "displayThumbnail": { + "type": "boolean", + "default": true + }, + "displayContext": { + "type": "boolean", + "default": false + }, + "period": { + "type": "string", + "default": "7" + }, + "postsToShow": { + "type": "number", + "default": 3 + }, + "postTypes": { + "type": "object", + "default": { + "post": true, + "page": true + } + } + } +} diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/controls.js b/projects/plugins/jetpack/extensions/blocks/top-posts/controls.js new file mode 100644 index 0000000000000..1f02d615b565e --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/controls.js @@ -0,0 +1,118 @@ +import { + PanelBody, + RangeControl, + SelectControl, + ToggleControl, + ToolbarGroup, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; + +export function TopPostsInspectorControls( { + attributes, + setAttributes, + postTypesData, + toggleAttributes, + setToggleAttributes, +} ) { + const { displayAuthor, displayContext, displayDate, displayThumbnail, period, postsToShow } = + attributes; + + if ( ! postTypesData ) { + return; + } + + const handleToggleChange = toggleId => isChecked => { + setToggleAttributes( prevAttributes => ( { + ...prevAttributes, + [ toggleId ]: isChecked, + } ) ); + + setAttributes( { postTypes: { ...toggleAttributes, [ toggleId ]: isChecked } } ); + }; + + const periodOptions = [ + { label: __( 'Last 24 hours', 'jetpack' ), value: '1' }, + { label: __( 'Last 48 hours', 'jetpack' ), value: '2' }, + { label: __( 'Last 7 days', 'jetpack' ), value: '7' }, + { label: __( 'Last 30 days', 'jetpack' ), value: '30' }, + { label: __( 'Last 90 days', 'jetpack' ), value: '90' }, + { label: __( 'Last year', 'jetpack' ), value: '365' }, + { label: __( 'All-time', 'jetpack' ), value: 'all-time' }, + ]; + + return ( + <> + + setAttributes( { postsToShow: Math.min( value, 10 ) } ) } + min={ 1 } + max={ 10 } + /> + setAttributes( { period: value } ) } + options={ periodOptions } + /> + + + { postTypesData.map( toggle => ( + + ) ) } + + + setAttributes( { displayDate: value } ) } + /> + setAttributes( { displayAuthor: value } ) } + /> + setAttributes( { displayContext: value } ) } + /> + setAttributes( { displayThumbnail: value } ) } + /> + + + ); +} + +export function TopPostsBlockControls( { attributes, setAttributes } ) { + const { layout } = attributes; + const layoutControls = [ + { + icon: 'grid-view', + title: __( 'Grid view', 'jetpack' ), + onClick: () => setAttributes( { layout: 'grid' } ), + isActive: layout === 'grid', + }, + { + icon: 'list-view', + title: __( 'List view', 'jetpack' ), + onClick: () => setAttributes( { layout: 'list' } ), + isActive: layout === 'list', + }, + ]; + + return ; +} diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/edit.js b/projects/plugins/jetpack/extensions/blocks/top-posts/edit.js new file mode 100644 index 0000000000000..1c0128c628197 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/edit.js @@ -0,0 +1,175 @@ +import { useModuleStatus } from '@automattic/jetpack-shared-extension-utils'; +import apiFetch from '@wordpress/api-fetch'; +import { BlockControls, InspectorControls } from '@wordpress/block-editor'; +import { useState, useEffect } from '@wordpress/element'; +import classNames from 'classnames'; +import { LoadingPostsGrid } from '../../shared/components/loading-posts-grid'; +import { TopPostsBlockControls, TopPostsInspectorControls } from './controls'; +import { InactiveStatsPlaceholder } from './inactive-placeholder'; +import './style.scss'; +import './editor.scss'; + +function TopPostsPreviewItem( props ) { + return ( +
+ { props.displayThumbnail && props.thumbnail && ( + + { + + ) } + { props.displayThumbnail && ! props.thumbnail && ( + +
+
+ ) } + { props.title } + { props.displayDate && ( + { props.date } + ) } + { props.displayAuthor && ( + { props.author } + ) } + { props.displayContext && props.context && ( + + { props.context[ 0 ].cat_name } + + ) } +
+ ); +} + +function TopPostsEdit( { attributes, className, setAttributes } ) { + const { isLoadingModules, isChangingStatus, isModuleActive, changeStatus } = + useModuleStatus( 'stats' ); + + const [ postsData, setPostsData ] = useState(); + const [ postsToDisplay, setPostsToDisplay ] = useState(); + const [ postTypesData, setPostTypesData ] = useState(); + const [ toggleAttributes, setToggleAttributes ] = useState( {} ); + + const { + displayAuthor, + displayContext, + displayDate, + displayThumbnail, + layout, + period, + postsToShow, + postTypes, + } = attributes; + + useEffect( () => { + apiFetch( { path: `/wpcom/v2/post-types` } ).then( response => { + setPostTypesData( response ); + response.forEach( type => { + if ( postTypes && postTypes[ type.id ] ) { + setToggleAttributes( prevToggleAttributes => ( { + ...prevToggleAttributes, + [ type.id ]: true, + } ) ); + } + } ); + } ); + }, [ postTypes ] ); + + useEffect( () => { + if ( isModuleActive ) { + apiFetch( { + path: `/wpcom/v2/top-posts?period=${ period }`, + } ).then( response => { + setPostsData( response ); + } ); + } + }, [ period, isModuleActive ] ); + + useEffect( () => { + const data = postsData; + + if ( ! data ) { + return; + } + + const newPosts = []; + for ( let i = 0; newPosts.length !== postsToShow; i++ ) { + if ( data[ i ] && postTypes[ data[ i ].type ] ) { + newPosts.push( + + ); + } + + // Out of posts. + if ( ! data[ i ] ) { + break; + } + } + + setPostsToDisplay( newPosts ); + }, [ + displayAuthor, + displayContext, + displayDate, + displayThumbnail, + postsData, + postTypes, + postsToShow, + setPostsToDisplay, + ] ); + + if ( ! isModuleActive && ! isLoadingModules ) { + return ( + + ); + } + + if ( ! postsToDisplay ) { + return ; + } + + return ( + <> + + + + + + + + +
+
{ postsToDisplay }
+
+ + ); +} + +export default TopPostsEdit; diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/editor.js b/projects/plugins/jetpack/extensions/blocks/top-posts/editor.js new file mode 100644 index 0000000000000..5124c339135af --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/editor.js @@ -0,0 +1,8 @@ +import { registerJetpackBlockFromMetadata } from '../../shared/register-jetpack-block'; +import metadata from './block.json'; +import edit from './edit'; + +registerJetpackBlockFromMetadata( metadata, { + edit, + save: () => null, +} ); diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/editor.scss b/projects/plugins/jetpack/extensions/blocks/top-posts/editor.scss new file mode 100644 index 0000000000000..e1eb3d7a27fa2 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/editor.scss @@ -0,0 +1,7 @@ +/** + * Editor styles for Top Posts + */ + +.wp-block-jetpack-top-posts .components-placeholder__fieldset { + display: block; +} diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/inactive-placeholder.js b/projects/plugins/jetpack/extensions/blocks/top-posts/inactive-placeholder.js new file mode 100644 index 0000000000000..85cf44dd212fc --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/inactive-placeholder.js @@ -0,0 +1,42 @@ +import { isAtomicSite, getBlockIconComponent } from '@automattic/jetpack-shared-extension-utils'; +import { Button, ExternalLink, Placeholder } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import metadata from './block.json'; + +export const InactiveStatsPlaceholder = ( { className, isLoading, changeStatus } ) => { + const enableFeature = () => { + return changeStatus( true ); + }; + + // Stats cannot be disabled on Simple sites, but they can on Atomic. + const supportLink = isAtomicSite() + ? 'https://wordpress.com/support/stats/' + : 'https://jetpack.com/support/jetpack-stats/'; + + return ( +
+ + +
+ + { __( 'Learn more about the Stats module.', 'jetpack' ) } + +
+
+
+ ); +}; diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/style.scss b/projects/plugins/jetpack/extensions/blocks/top-posts/style.scss new file mode 100644 index 0000000000000..cee8468e47b41 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/style.scss @@ -0,0 +1,76 @@ +/** + * Styles for Top Posts + */ +@import '@automattic/jetpack-base-styles/gutenberg-base-styles'; +@import '../../shared/styles/jetpack-variables.scss'; + +.wp-block-jetpack-top-posts { + margin-bottom: $jetpack-block-margin-bottom; + + img { + width: 100%; + } + + a { + display: inline; + } + + span { + display: block; + } + + &.is-list-layout .jetpack-top-posts-item { + margin-bottom: $jetpack-block-margin-bottom; + } + + &.is-grid-layout { + .jetpack-top-posts-wrapper { + align-items: flex-start; + display: grid; + grid: auto / repeat( 6, 1fr ); + gap: 16px 12px; + + @media only screen and ( max-width: $break-small ) { + display: block; + + .jetpack-top-posts-mock-thumbnail { + display: none; + } + + .jetpack-top-posts-item { + margin-bottom: $jetpack-block-margin-bottom; + } + } + } + + .jetpack-top-posts-mock-thumbnail { + background-color: $gray-100; + height: 0; + padding-bottom: 65%; + position: relative; + width: 100%; + } + + .jetpack-top-posts-item { + grid-column: span 2; + } + + // These rows should display two items. + @for $i from 2 through 7 { + @if $i == 2 or $i == 4 { + &[data-item-count='#{$i}'] .jetpack-top-posts-item { + grid-column: span 3; + } + } + + @if $i == 5 or $i == 7 { + &[data-item-count='#{$i}'] .jetpack-top-posts-item:nth-child( 5n ), + &[data-item-count='#{$i}'] .jetpack-top-posts-item:nth-child( 5n-1 ), + &[data-item-count='#{$i}'] .jetpack-top-posts-item:nth-child( #{$i}n ), + &[data-item-count='#{$i}'] .jetpack-top-posts-item:nth-child( #{$i}n-1 ) { + grid-column: span 3; + } + } + } + } +} diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/test/controls.js b/projects/plugins/jetpack/extensions/blocks/top-posts/test/controls.js new file mode 100644 index 0000000000000..cd3ef56814ce0 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/test/controls.js @@ -0,0 +1,135 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { TopPostsInspectorControls, TopPostsBlockControls } from '../controls'; + +jest.mock( '@wordpress/api-fetch' ); + +jest.mock( '@automattic/jetpack-shared-extension-utils' ); + +describe( 'TopPostsControls', () => { + const defaultAttributes = { + displayAuthor: true, + displayContext: true, + displayDate: true, + displayThumbnail: true, + layout: 'grid', + period: '7', + postsToShow: 3, + postTypes: { + post: true, + page: false, + }, + }; + + const setAttributes = jest.fn(); + const defaultProps = { + attributes: defaultAttributes, + postTypesData: [ + { label: 'Posts', id: 'post' }, + { label: 'Pages', id: 'page' }, + { label: 'Media', id: 'attachment' }, + ], + setAttributes, + toggleAttributes: jest.fn(), + setToggleAttributes: jest.fn(), + }; + + beforeEach( () => { + setAttributes.mockClear(); + } ); + + describe( 'Inspector settings', () => { + test( 'displays custom content types', () => { + render( ); + + expect( screen.getByText( 'Display posts' ) ).toBeInTheDocument(); + expect( screen.getByText( 'Display pages' ) ).toBeInTheDocument(); + expect( screen.getByText( 'Display media' ) ).toBeInTheDocument(); + } ); + + test( 'displays correct content types', () => { + const overriddenProps = { + ...defaultProps, + postTypesData: [ + { label: 'Posts', id: 'post' }, + { label: 'Pages', id: 'page' }, + { label: 'Portfolios', id: 'portfolio' }, + ], + }; + render( ); + + expect( screen.getByText( 'Display portfolios' ) ).toBeInTheDocument(); + expect( screen.queryByText( 'Display media' ) ).not.toBeInTheDocument(); + } ); + + test( 'displays number of items slider', () => { + render( ); + + expect( screen.getByText( 'Number of items' ) ).toBeInTheDocument(); + } ); + + test( 'sets postsToShow attribute', async () => { + const user = userEvent.setup(); + render( ); + const input = screen.getAllByLabelText( 'Number of items' )[ 1 ]; + await user.type( input, '10' ); + + expect( setAttributes ).toHaveBeenCalledWith( { postsToShow: 10 } ); + } ); + + test( 'sets stats period', async () => { + const user = userEvent.setup(); + render( ); + await user.selectOptions( screen.getByLabelText( 'Stats period' ), [ 'Last 48 hours' ] ); + + expect( setAttributes ).toHaveBeenCalledWith( { period: '2' } ); + } ); + + test( 'displays Thumbnail display toggle', () => { + render( ); + + expect( screen.getByLabelText( 'Display thumbnail' ) ).toBeInTheDocument(); + } ); + + test( 'sets displayThumbnail attribute', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'Display thumbnail' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { displayThumbnail: false } ); + } ); + + test( 'displays Date display toggle', () => { + render( ); + + expect( screen.getByLabelText( 'Display date' ) ).toBeInTheDocument(); + } ); + + test( 'sets displayDate attribute', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'Display date' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { displayDate: false } ); + } ); + } ); + + describe( 'Toolbar settings', () => { + const props = { ...defaultProps, context: 'toolbar' }; + + test( 'loads and displays layout buttons in toolbar', () => { + render( ); + + expect( screen.getByLabelText( 'Grid view' ) ).toBeInTheDocument(); + expect( screen.getByLabelText( 'List view' ) ).toBeInTheDocument(); + } ); + + test( 'sets the layout attribute', async () => { + const user = userEvent.setup(); + render( ); + await user.click( screen.getByLabelText( 'List view' ) ); + + expect( setAttributes ).toHaveBeenCalledWith( { layout: 'list' } ); + } ); + } ); +} ); diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/test/edit.js b/projects/plugins/jetpack/extensions/blocks/top-posts/test/edit.js new file mode 100644 index 0000000000000..37d920b835750 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/test/edit.js @@ -0,0 +1,238 @@ +import { useModuleStatus } from '@automattic/jetpack-shared-extension-utils'; +import { render, screen, waitFor } from '@testing-library/react'; +import apiFetch from '@wordpress/api-fetch'; +import TopPostsEdit from '../edit'; + +jest.mock( '@wordpress/api-fetch' ); + +jest.mock( '@automattic/jetpack-shared-extension-utils' ); + +const defaultAttributes = { + displayAuthor: true, + displayContext: true, + displayDate: true, + displayThumbnail: true, + layout: 'grid', + period: '7', + postsToShow: 3, + postTypes: { + post: true, + page: false, + }, +}; + +const defaultProps = { + attributes: defaultAttributes, + setAttributes: jest.fn(), +}; + +beforeEach( () => { + apiFetch.mockReturnValue( + Promise.resolve( [ + { + id: 1, + author: 'Writer 1', + context: [ + { + term_id: 1, + name: 'Category', + slug: 'category', + term_group: 0, + term_taxonomy_id: 1, + taxonomy: 'category', + description: '', + parent: 0, + count: 5, + filter: 'raw', + cat_ID: 1, + category_count: 5, + category_description: '', + cat_name: 'Category', + category_nicename: 'category', + category_parent: 0, + }, + ], + href: 'https://test.com', + date: '20 Nov 2023', + title: 'Post Title 1', + type: 'post', + public: true, + views: 30, + video_play: false, + thumbnail: 'https://test.com/image1.png', + }, + { + id: 2, + author: 'Writer 2', + context: [ + { + term_id: 1, + name: 'Uncategorized', + slug: 'uncategorized', + term_group: 0, + term_taxonomy_id: 1, + taxonomy: 'category', + description: '', + parent: 0, + count: 5, + filter: 'raw', + cat_ID: 1, + category_count: 5, + category_description: '', + cat_name: 'Uncategorized', + category_nicename: 'uncategorized', + category_parent: 0, + }, + ], + href: 'https://test.com', + date: '19 Nov 2023', + title: 'Post Title 2', + type: 'post', + public: true, + views: 30, + video_play: false, + thumbnail: 'https://test.com/image2.png', + }, + { + id: 3, + author: 'Writer 3', + context: [ + { + term_id: 1, + name: 'Uncategorized', + slug: 'uncategorized', + term_group: 0, + term_taxonomy_id: 1, + taxonomy: 'category', + description: '', + parent: 0, + count: 5, + filter: 'raw', + cat_ID: 1, + category_count: 5, + category_description: '', + cat_name: 'Uncategorized', + category_nicename: 'uncategorized', + category_parent: 0, + }, + ], + href: 'https://test.com', + date: '18 Nov 2023', + title: 'Post Title 3', + type: 'post', + public: true, + views: 30, + video_play: false, + thumbnail: 'https://test.com/image3.png', + }, + ] ) + ); + + useModuleStatus.mockReturnValue( { + isModuleActive: true, + changeStatus: jest.fn(), + } ); +} ); + +describe( 'TopPostsEdit', () => { + /** + * Renders Top Posts. + * + * @param {object} attributeOverrides - Attribute overrides. + */ + function renderTopPosts( attributeOverrides = {} ) { + const attributes = { ...defaultAttributes, ...attributeOverrides }; + + render( ); + } + + test( 'renders post titles', async () => { + renderTopPosts(); + + await waitFor( () => { + expect( screen.getByText( 'Post Title 1' ) ).toBeInTheDocument(); + } ); + } ); + + test( 'renders post dates', async () => { + renderTopPosts(); + + await waitFor( () => { + expect( screen.getByText( '20 Nov 2023' ) ).toBeInTheDocument(); + } ); + } ); + + test( 'does not render date when setting is disabled', async () => { + renderTopPosts( { displayDate: false } ); + + await waitFor( () => { + expect( screen.queryByText( '20 Nov 2023' ) ).not.toBeInTheDocument(); + } ); + } ); + + test( 'renders post authors', async () => { + renderTopPosts(); + await waitFor( () => { + expect( screen.getByText( 'Writer 1' ) ).toBeInTheDocument(); + } ); + } ); + + test( 'does not render author when setting is disabled', async () => { + renderTopPosts( { displayAuthor: false } ); + + await waitFor( () => { + expect( screen.queryByText( 'Writer 1' ) ).not.toBeInTheDocument(); + } ); + } ); + + test( 'renders post thumbnails', async () => { + renderTopPosts(); + await waitFor( () => { + expect( screen.getByAltText( 'Post Title 1' ) ).toBeInTheDocument(); + } ); + } ); + + test( 'does not render thumbnails when setting is disabled', async () => { + renderTopPosts( { displayThumbnail: false } ); + + await waitFor( () => { + expect( screen.queryByAltText( 'Post Title 1' ) ).not.toBeInTheDocument(); + } ); + } ); + + test( 'renders post category', async () => { + renderTopPosts(); + + await waitFor( () => { + expect( screen.getByText( 'Category' ) ).toBeInTheDocument(); + } ); + } ); + + test( 'does not render post category when setting is disabled', async () => { + renderTopPosts( { displayContext: false } ); + + await waitFor( () => { + expect( screen.queryByText( 'Category' ) ).not.toBeInTheDocument(); + } ); + } ); + + test( 'does not render more posts than needed', async () => { + renderTopPosts( { postsToShow: 1 } ); + await waitFor( () => { + expect( screen.queryByText( 'Post Title 2' ) ).not.toBeInTheDocument(); + } ); + } ); + + test( 'renders option to activate stats when module is disabled', async () => { + useModuleStatus.mockReturnValue( { + isModuleActive: false, + changeStatus: jest.fn(), + } ); + + renderTopPosts(); + + await waitFor( () => { + expect( screen.getByText( 'Activate Stats' ) ).toBeInTheDocument(); + } ); + } ); +} ); diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.html b/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.html new file mode 100644 index 0000000000000..fc54467a0a9ec --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.html @@ -0,0 +1 @@ + diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.json b/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.json new file mode 100644 index 0000000000000..b29895270907f --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.json @@ -0,0 +1,22 @@ +[ + { + "clientId": "_clientId_0", + "name": "jetpack/top-posts", + "isValid": true, + "attributes": { + "layout": "grid", + "displayAuthor": true, + "displayDate": true, + "displayThumbnail": true, + "displayContext": false, + "period": "7", + "postsToShow": 3, + "postTypes": { + "post": true, + "page": false + } + }, + "innerBlocks": [], + "originalContent": "" + } +] diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.parsed.json b/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.parsed.json new file mode 100644 index 0000000000000..cefcc59c9422c --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.parsed.json @@ -0,0 +1,38 @@ +[ + { + "blockName": "jetpack/top-posts", + "attrs": { + "postTypes": { + "post": true, + "page": false + }, + "backgroundColor": "base", + "textColor": "contrast", + "fontFamily": "heading", + "fontSize": "medium", + "style": { + "elements": { + "link": { + "color": { + "text": "var:preset|color|contrast" + } + } + }, + "spacing": { + "padding": { + "top": "0", + "bottom": "0", + "left": "0", + "right": "0" + } + }, + "typography": { + "lineHeight": "1.3" + } + } + }, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } +] diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.serialized.html b/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.serialized.html new file mode 100644 index 0000000000000..285c70568ec0c --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/test/fixtures/jetpack__top__posts.serialized.html @@ -0,0 +1 @@ + diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/test/validate.js b/projects/plugins/jetpack/extensions/blocks/top-posts/test/validate.js new file mode 100644 index 0000000000000..2582ec88de967 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/test/validate.js @@ -0,0 +1,7 @@ +import runBlockFixtureTests from '../../../shared/test/block-fixtures'; +import metadata from '../block.json'; + +const { name } = metadata; +const blocks = [ { name, settings: metadata } ]; + +runBlockFixtureTests( name, blocks, __dirname ); diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/top-posts.php b/projects/plugins/jetpack/extensions/blocks/top-posts/top-posts.php new file mode 100644 index 0000000000000..6ca58742af177 --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/top-posts.php @@ -0,0 +1,113 @@ +has_connected_owner() + && ! ( new Status() )->is_offline_mode() ) + ) { + Blocks::jetpack_register_block( + __DIR__, + array( 'render_callback' => __NAMESPACE__ . '\load_assets' ) + ); + } +} +add_action( 'init', __NAMESPACE__ . '\register_block' ); + +/** + * Top Posts block registration/dependency declaration. + * + * @param array $attributes Array containing the Top Posts block attributes. + * + * @return string + */ +function load_assets( $attributes ) { + Jetpack_Gutenberg::load_assets_as_required( __DIR__ ); + + /* + * We cannot rely on obtaining posts from the block because + * top posts might have changed since then. As such, we must + * check for updated stats. + */ + $period = $attributes['period']; + $number = $attributes['postsToShow']; + $types = implode( ',', array_keys( array_filter( $attributes['postTypes'] ) ) ); + + $data = Jetpack_Top_Posts_Helper::get_top_posts( $period, $number, $types ); + + if ( ! is_array( $data ) ) { + return; + } + + $wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports(); + + $output = sprintf( + '
', + ! empty( $attributes['className'] ) ? ' ' . esc_attr( $attributes['className'] ) : '', + ! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '', + ' is-' . esc_attr( $attributes['layout'] ) . '-layout', + ! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : '', + count( $data ) + ); + + foreach ( $data as $item ) { + $output .= '
'; + + if ( $attributes['displayThumbnail'] ) { + $output .= ''; + + if ( ! empty( $item['thumbnail'] ) ) { + $output .= '' . esc_attr( $item['title'] ) . ''; + } else { + $output .= '
'; + } + + $output .= '
'; + } + + $output .= '' . esc_html( $item['title'] ) . ''; + + if ( $attributes['displayDate'] ) { + $output .= '' . esc_html( $item['date'] ) . ''; + } + + if ( $attributes['displayAuthor'] ) { + $output .= '' . esc_html( $item['author'] ) . ''; + } + + if ( $attributes['displayContext'] && ! empty( $item['context'] ) && is_array( $item['context'] ) ) { + $context = reset( $item['context'] ); + $output .= '' . esc_html( $context->name ) . ''; + } + + $output .= '
'; + } + + $output .= '
'; + + return $output; +} diff --git a/projects/plugins/jetpack/extensions/blocks/top-posts/view.js b/projects/plugins/jetpack/extensions/blocks/top-posts/view.js new file mode 100644 index 0000000000000..423b033ce717c --- /dev/null +++ b/projects/plugins/jetpack/extensions/blocks/top-posts/view.js @@ -0,0 +1 @@ +import './style.scss'; diff --git a/projects/plugins/jetpack/extensions/index.json b/projects/plugins/jetpack/extensions/index.json index 766c47e0283f2..6785f62b23918 100644 --- a/projects/plugins/jetpack/extensions/index.json +++ b/projects/plugins/jetpack/extensions/index.json @@ -67,6 +67,7 @@ "v6-video-frame-poster", "videopress/video-chapters", "create-with-voice", + "top-posts", "ai-assistant-backend-prompts", "sharing-button", "sharing-buttons" diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx index d126f56f02f13..081993daec6c3 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-assistant-plugin-sidebar/index.tsx @@ -18,6 +18,7 @@ import useAiFeature, { import JetpackPluginSidebar from '../../../../shared/jetpack-plugin-sidebar'; import Proofread from '../proofread'; import UsagePanel from '../usage-panel'; +import { USAGE_PANEL_PLACEMENT_JETPACK_SIDEBAR } from '../usage-panel/types'; // Determine if the usage panel is enabled or not const isUsagePanelAvailable = @@ -90,7 +91,7 @@ export default function AiAssistantPluginSidebar() { ) } { isUsagePanelAvailable && ( - + ) } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-bar/style.scss b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-bar/style.scss index 67ab35d034dea..dffe67c085ed4 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-bar/style.scss +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-bar/style.scss @@ -3,6 +3,10 @@ .jetpack-ai-usage-panel { --spacing-base: 8px; + .components-base-control { + margin: 0; + } + .jetpack-ai-usage-panel-loading { height: calc( var( --spacing-base ) * 11 + 0.5px ); background-color: var( --jp-gray ); diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-panel/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-panel/index.tsx index dc8c0e93d9dfe..0d90e713c9f94 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-panel/index.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-panel/index.tsx @@ -4,6 +4,7 @@ import { getRedirectUrl } from '@automattic/jetpack-components'; import { isAtomicSite, isSimpleSite } from '@automattic/jetpack-shared-extension-utils'; import { Button } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import React from 'react'; /** @@ -12,11 +13,13 @@ import React from 'react'; import './style.scss'; import useAICheckout from '../../../../blocks/ai-assistant/hooks/use-ai-checkout'; import useAiFeature from '../../../../blocks/ai-assistant/hooks/use-ai-feature'; +import useAnalytics from '../../../../blocks/ai-assistant/hooks/use-analytics'; import { canUserPurchasePlan } from '../../../../blocks/ai-assistant/lib/connection'; import useAutosaveAndRedirect from '../../../../shared/use-autosave-and-redirect'; import UsageControl from '../usage-bar'; import './style.scss'; import { PLAN_TYPE_FREE, PLAN_TYPE_TIERED, PLAN_TYPE_UNLIMITED } from '../usage-bar/types'; +import type { UsagePanelProps } from './types'; import type { PlanType } from '../usage-bar/types'; /** @@ -119,9 +122,10 @@ const useContactUsLink = (): { }; }; -export default function UsagePanel() { +export default function UsagePanel( { placement = null }: UsagePanelProps ) { const { checkoutUrl, autosaveAndRedirect, isRedirecting } = useAICheckout(); const { contactUsURL, autosaveAndRedirectContactUs } = useContactUsLink(); + const { tracks } = useAnalytics(); const canUpgrade = canUserPurchasePlan(); // fetch usage data @@ -140,6 +144,32 @@ export default function UsagePanel() { planType === PLAN_TYPE_TIERED ? usagePeriod?.requestsCount : allTimeRequestsCount; const requestsLimit = planType === PLAN_TYPE_FREE ? freeRequestsLimit : currentTier?.limit; + const trackUpgradeClick = useCallback( + ( event: React.MouseEvent< HTMLElement > ) => { + event.preventDefault(); + tracks.recordEvent( 'jetpack_ai_usage_panel_upgrade_button_click', { + current_tier_slug: currentTier?.slug, + requests_count: requestsCount, + ...( placement ? { placement } : {} ), + } ); + autosaveAndRedirect( event ); + }, + [ tracks, currentTier, requestsCount, placement, autosaveAndRedirect ] + ); + + const trackContactUsClick = useCallback( + ( event: React.MouseEvent< HTMLElement > ) => { + event.preventDefault(); + tracks.recordEvent( 'jetpack_ai_usage_panel_upgrade_button_click', { + current_tier_slug: currentTier?.slug, + requests_count: requestsCount, + ...( placement ? { placement } : {} ), + } ); + autosaveAndRedirectContactUs(); + }, + [ tracks, currentTier, requestsCount, placement, autosaveAndRedirectContactUs ] + ); + // Determine the upgrade button text const upgradeButtonText = useUpgradeButtonText( planType, nextTier?.limit ); @@ -167,7 +197,7 @@ export default function UsagePanel() { variant="primary" label={ __( 'Contact us for more requests', 'jetpack' ) } href={ contactUsURL } - onClick={ autosaveAndRedirectContactUs } + onClick={ trackContactUsClick } > { __( 'Contact Us', 'jetpack' ) } @@ -178,7 +208,7 @@ export default function UsagePanel() { variant="primary" label={ __( 'Upgrade your Jetpack AI plan', 'jetpack' ) } href={ checkoutUrl } - onClick={ autosaveAndRedirect } + onClick={ trackUpgradeClick } disabled={ isRedirecting } > { upgradeButtonText } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-panel/types.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-panel/types.ts new file mode 100644 index 0000000000000..f744961607c5a --- /dev/null +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/usage-panel/types.ts @@ -0,0 +1,11 @@ +export const USAGE_PANEL_PLACEMENT_JETPACK_SIDEBAR = 'jetpack_sidebar'; +export const USAGE_PANEL_PLACEMENT_BLOCK_SETTINGS_SIDEBAR = 'block_settings_sidebar'; + +/* + * Props for the usage panel component. + */ +export type UsagePanelProps = { + placement?: + | typeof USAGE_PANEL_PLACEMENT_JETPACK_SIDEBAR + | typeof USAGE_PANEL_PLACEMENT_BLOCK_SETTINGS_SIDEBAR; +}; diff --git a/projects/plugins/jetpack/extensions/plugins/launchpad-save-modal/index.js b/projects/plugins/jetpack/extensions/plugins/launchpad-save-modal/index.js index ba089e501f3d5..4a86baaf13651 100644 --- a/projects/plugins/jetpack/extensions/plugins/launchpad-save-modal/index.js +++ b/projects/plugins/jetpack/extensions/plugins/launchpad-save-modal/index.js @@ -3,6 +3,7 @@ import { getSiteFragment, useAnalytics } from '@automattic/jetpack-shared-extens import apiFetch from '@wordpress/api-fetch'; import { Modal, Button, CheckboxControl } from '@wordpress/components'; import { usePrevious } from '@wordpress/compose'; +import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; import { useEffect, useRef, useState } from '@wordpress/element'; @@ -40,13 +41,22 @@ const updateLaunchpadSaveModalBrowserConfig = config => { export const settings = { render: function LaunchpadSaveModal() { const { isSavingSite, isSavingPost, isCurrentPostPublished, postLink, postType } = useSelect( - selector => ( { - isSavingSite: selector( editorStore ).isSavingNonPostEntityChanges(), - isSavingPost: selector( editorStore ).isSavingPost(), - isCurrentPostPublished: selector( editorStore ).isCurrentPostPublished(), - postLink: selector( editorStore ).getPermalink(), - postType: selector( editorStore ).getCurrentPostType(), - } ) + select => { + const { __experimentalGetDirtyEntityRecords, isSavingEntityRecord } = select( coreStore ); + const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); + + return { + // Align the “Save” behavior in the editor. + // See https://github.com/WordPress/gutenberg/blob/96305e952948653e2921147492556d09ee9d3c17/packages/edit-site/src/components/save-button/index.js#L43 + isSavingSite: dirtyEntityRecords.some( record => + isSavingEntityRecord( record.kind, record.name, record.key ) + ), + isSavingPost: select( editorStore ).isSavingPost(), + isCurrentPostPublished: select( editorStore ).isCurrentPostPublished(), + postLink: select( editorStore ).getPermalink(), + postType: select( editorStore ).getCurrentPostType(), + }; + } ); const prevIsSavingSite = usePrevious( isSavingSite ); diff --git a/projects/plugins/jetpack/extensions/blocks/related-posts/skeleton-loader.js b/projects/plugins/jetpack/extensions/shared/components/loading-posts-grid/index.js similarity index 95% rename from projects/plugins/jetpack/extensions/blocks/related-posts/skeleton-loader.js rename to projects/plugins/jetpack/extensions/shared/components/loading-posts-grid/index.js index 7f0e016a1c545..d1f34a6461bbb 100644 --- a/projects/plugins/jetpack/extensions/blocks/related-posts/skeleton-loader.js +++ b/projects/plugins/jetpack/extensions/shared/components/loading-posts-grid/index.js @@ -1,7 +1,7 @@ import { LoadingPlaceholder, ThemeProvider } from '@automattic/jetpack-components'; import { Placeholder, Flex, FlexItem } from '@wordpress/components'; -export const RelatedPostsSkeletonLoader = () => { +export const LoadingPostsGrid = () => { return ( diff --git a/projects/plugins/jetpack/modules/likes.php b/projects/plugins/jetpack/modules/likes.php index 2d75650fee706..3b0bc4f4975a9 100644 --- a/projects/plugins/jetpack/modules/likes.php +++ b/projects/plugins/jetpack/modules/likes.php @@ -464,7 +464,15 @@ function_exists( '\Activitypub\is_activitypub_request' ) // Let's make sure that the script is enqueued. wp_enqueue_script( 'jetpack_likes_queuehandler' ); - return $content . $html; + $beta_blocks_active = defined( 'JETPACK_BLOCKS_VARIATION' ) && JETPACK_BLOCKS_VARIATION === 'beta'; + + // If we're using a block-based theme and the Like block (beta) is available, return content without the Like button widget. + // Otherwise return content with the Like button widget. + if ( wp_is_block_theme() && $beta_blocks_active ) { + return $content; + } else { + return $content . $html; + } } /** Checks if admin bar is visible.*/ diff --git a/projects/plugins/jetpack/modules/likes/style.css b/projects/plugins/jetpack/modules/likes/style.css index 01e14726b91ec..965996fe1c0c2 100644 --- a/projects/plugins/jetpack/modules/likes/style.css +++ b/projects/plugins/jetpack/modules/likes/style.css @@ -177,7 +177,7 @@ div.jetpack-comment-likes-widget-wrapper iframe { #likes-other-gravatars.wpl-new-layout ul.wpl-avatars li a img { background: none; - border: 1px solid #fff;; + border: none; border-radius: 50%; margin: 0 !important; padding: 1px !important; diff --git a/projects/plugins/jetpack/modules/widgets/top-posts.php b/projects/plugins/jetpack/modules/widgets/top-posts.php index 3be2be2294d79..6d3fed348137f 100644 --- a/projects/plugins/jetpack/modules/widgets/top-posts.php +++ b/projects/plugins/jetpack/modules/widgets/top-posts.php @@ -83,6 +83,18 @@ public function __construct() { * @since 3.9.3 */ add_action( 'jetpack_widget_top_posts_after_fields', array( $this, 'stats_explanation' ) ); + add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_widget_in_block_editor' ) ); + } + + /** + * Remove the "Top Posts and Pages" widget from the Legacy Widget block + * + * @param array $widget_types List of widgets that are currently removed from the Legacy Widget block. + * @return array $widget_types New list of widgets that will be removed. + */ + public function hide_widget_in_block_editor( $widget_types ) { + $widget_types[] = 'top-posts'; + return $widget_types; } /** diff --git a/projects/plugins/jetpack/tests/php/extensions/blocks/premium-content/test_class.jetpack-premium-content.php b/projects/plugins/jetpack/tests/php/extensions/blocks/premium-content/test_class.jetpack-premium-content.php index 6e3b13376865f..f5193e197b8d8 100644 --- a/projects/plugins/jetpack/tests/php/extensions/blocks/premium-content/test_class.jetpack-premium-content.php +++ b/projects/plugins/jetpack/tests/php/extensions/blocks/premium-content/test_class.jetpack-premium-content.php @@ -60,7 +60,7 @@ private function get_payload( $is_subscribed, $is_paid_subscriber, $subscription } /** - * Stubs WPCOM_Token_Subscription_Service in order to return the provided token. + * Stubs Jetpack_Token_Subscription_Service in order to return the provided token. * * @param array $payload * @return mixed diff --git a/projects/plugins/jetpack/tests/php/modules/subscriptions/test_class.jetpack-subscriptions.php b/projects/plugins/jetpack/tests/php/modules/subscriptions/test_class.jetpack-subscriptions.php index d80e3738abfb5..45158e9f014a9 100644 --- a/projects/plugins/jetpack/tests/php/modules/subscriptions/test_class.jetpack-subscriptions.php +++ b/projects/plugins/jetpack/tests/php/modules/subscriptions/test_class.jetpack-subscriptions.php @@ -9,7 +9,7 @@ use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\Token_Subscription_Service; use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\WPCOM_Offline_Subscription_Service; use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\WPCOM_Online_Subscription_Service; -use Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service\WPCOM_Token_Subscription_Service; +use Tests\Automattic\Jetpack\Extensions\Premium_Content\Test_Jetpack_Token_Subscription_Service; use function Automattic\Jetpack\Extensions\Subscriptions\register_block as register_subscription_block; use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_LEVEL_ACCESS_SETTINGS; use const Automattic\Jetpack\Extensions\Subscriptions\META_NAME_FOR_POST_TIER_ID_SETTINGS; @@ -91,9 +91,9 @@ public function test_jetpack_cookie_user() { ) ); - unset( $_COOKIE[ WPCOM_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME ] ); + unset( $_COOKIE[ Test_Jetpack_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME ] ); - $this->assertNotContains( WPCOM_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME, $_COOKIE ); + $this->assertNotContains( Test_Jetpack_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME, $_COOKIE ); $this->assertTrue( $token_subscription_service->visitor_can_view_content( array( $this->plan_id ), '' ) ); $this->assertTrue( $token_subscription_service->visitor_can_view_content( array( $this->plan_id ), 'everybody' ) ); @@ -101,7 +101,7 @@ public function test_jetpack_cookie_user() { $this->assertTrue( $token_subscription_service->visitor_can_view_content( array( $this->plan_id ), 'paid_subscribers' ) ); // Now we make sure we have the cookie - $this->assertNotNull( $_COOKIE[ WPCOM_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME ] ); + $this->assertNotNull( $_COOKIE[ Test_Jetpack_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME ] ); // We remove the token unset( $_GET['token'] ); @@ -113,7 +113,7 @@ public function test_jetpack_cookie_user() { $this->assertTrue( $token_subscription_service->visitor_can_view_content( array( $this->plan_id ), 'paid_subscribers' ) ); // We remove the cookie - unset( $_COOKIE[ WPCOM_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME ] ); + unset( $_COOKIE[ Test_Jetpack_Token_Subscription_Service::JWT_AUTH_TOKEN_COOKIE_NAME ] ); // We make sure everything nothing works anymore $this->assertTrue( $token_subscription_service->visitor_can_view_content( array( $this->plan_id ), '' ) ); @@ -238,7 +238,7 @@ public function matrix_access() { } /** - * Stubs WPCOM_Token_Subscription_Service in order to return the provided token. + * Stubs Test_Jetpack_Token_Subscription_Service in order to return the provided token. * * @param array $payload * @return mixed @@ -246,7 +246,7 @@ public function matrix_access() { private function set_returned_token( $payload ) { // We remove anything else remove_all_filters( 'earn_get_user_subscriptions_for_site_id' ); - $service = new WPCOM_Token_Subscription_Service(); + $service = new Test_Jetpack_Token_Subscription_Service(); $_GET['token'] = JWT::encode( $payload, $service->get_key() ); return $service; } @@ -310,7 +310,7 @@ public function test_subscriber_access_level( $type_user_id, $logged, $token_set if ( $token_set ) { $this->set_returned_token( $payload ); - $token_subscription_service = new WPCOM_Token_Subscription_Service(); + $token_subscription_service = new Test_Jetpack_Token_Subscription_Service(); $result = $token_subscription_service->visitor_can_view_content( array( $this->plan_id ), $post_access_level ); } else { if ( $logged ) { diff --git a/projects/plugins/social/changelog/add-source-to-jetpack-connect-url b/projects/plugins/migration/changelog/add-send-secret-url-is-ip similarity index 100% rename from projects/plugins/social/changelog/add-source-to-jetpack-connect-url rename to projects/plugins/migration/changelog/add-send-secret-url-is-ip diff --git a/projects/plugins/social/changelog/add-verbum-subscription-modal-settings b/projects/plugins/migration/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/add-verbum-subscription-modal-settings rename to projects/plugins/migration/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/migration/composer.lock b/projects/plugins/migration/composer.lock index 090ce607269ee..fca27786e437e 100644 --- a/projects/plugins/migration/composer.lock +++ b/projects/plugins/migration/composer.lock @@ -124,7 +124,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "335ee318534ab58327f45abc5da1748a76dd531d" + "reference": "1f713fb83a98dab43f325fe71331d40f9ca47334" }, "require": { "automattic/jetpack-constants": "@dev", @@ -133,7 +133,7 @@ "require-dev": { "automattic/jetpack-changelogger": "@dev", "brain/monkey": "2.6.1", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -615,7 +615,7 @@ "dist": { "type": "path", "url": "../../packages/identity-crisis", - "reference": "39eb9595c27599391f98ef53ead52300ccfe2901" + "reference": "e7f53dc4d861086ba733fc32571824c19350b9ca" }, "require": { "automattic/jetpack-assets": "@dev", @@ -645,7 +645,7 @@ "link-template": "https://github.com/Automattic/jetpack-identity-crisis/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.13.x-dev" + "dev-trunk": "0.14.x-dev" } }, "autoload": { @@ -1417,7 +1417,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -1425,7 +1425,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/social/changelog/earn-remove-connected-account-id-option-2 b/projects/plugins/mu-wpcom-plugin/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/earn-remove-connected-account-id-option-2 rename to projects/plugins/mu-wpcom-plugin/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/mu-wpcom-plugin/composer.lock b/projects/plugins/mu-wpcom-plugin/composer.lock index b1a9d1d997227..640b6c45032fe 100644 --- a/projects/plugins/mu-wpcom-plugin/composer.lock +++ b/projects/plugins/mu-wpcom-plugin/composer.lock @@ -81,7 +81,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -89,7 +89,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/social/changelog/fix-autoloader-self-version b/projects/plugins/protect/changelog/add-send-secret-url-is-ip similarity index 100% rename from projects/plugins/social/changelog/fix-autoloader-self-version rename to projects/plugins/protect/changelog/add-send-secret-url-is-ip diff --git a/projects/plugins/social/changelog/fix-connection-partner-standalone-sites b/projects/plugins/protect/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/fix-connection-partner-standalone-sites rename to projects/plugins/protect/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/protect/composer.lock b/projects/plugins/protect/composer.lock index 22fe84b3a417d..d5a8fd616f62f 100644 --- a/projects/plugins/protect/composer.lock +++ b/projects/plugins/protect/composer.lock @@ -124,7 +124,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "335ee318534ab58327f45abc5da1748a76dd531d" + "reference": "1f713fb83a98dab43f325fe71331d40f9ca47334" }, "require": { "automattic/jetpack-constants": "@dev", @@ -133,7 +133,7 @@ "require-dev": { "automattic/jetpack-changelogger": "@dev", "brain/monkey": "2.6.1", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -529,7 +529,7 @@ "dist": { "type": "path", "url": "../../packages/identity-crisis", - "reference": "39eb9595c27599391f98ef53ead52300ccfe2901" + "reference": "e7f53dc4d861086ba733fc32571824c19350b9ca" }, "require": { "automattic/jetpack-assets": "@dev", @@ -559,7 +559,7 @@ "link-template": "https://github.com/Automattic/jetpack-identity-crisis/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.13.x-dev" + "dev-trunk": "0.14.x-dev" } }, "autoload": { @@ -1524,7 +1524,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -1532,7 +1532,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/social/changelog/fix-idc-search-replace-protection b/projects/plugins/search/changelog/add-send-secret-url-is-ip similarity index 100% rename from projects/plugins/social/changelog/fix-idc-search-replace-protection rename to projects/plugins/search/changelog/add-send-secret-url-is-ip diff --git a/projects/plugins/social/changelog/fix-videopress-upgrade-path b/projects/plugins/search/changelog/add-top-post-pages-block similarity index 100% rename from projects/plugins/social/changelog/fix-videopress-upgrade-path rename to projects/plugins/search/changelog/add-top-post-pages-block diff --git a/projects/plugins/social/changelog/jitm-redirection-after-activate-click b/projects/plugins/search/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/jitm-redirection-after-activate-click rename to projects/plugins/search/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/search/composer.lock b/projects/plugins/search/composer.lock index 15b0055fade67..3ba86d378d609 100644 --- a/projects/plugins/search/composer.lock +++ b/projects/plugins/search/composer.lock @@ -124,7 +124,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "335ee318534ab58327f45abc5da1748a76dd531d" + "reference": "1f713fb83a98dab43f325fe71331d40f9ca47334" }, "require": { "automattic/jetpack-constants": "@dev", @@ -133,7 +133,7 @@ "require-dev": { "automattic/jetpack-changelogger": "@dev", "brain/monkey": "2.6.1", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -529,7 +529,7 @@ "dist": { "type": "path", "url": "../../packages/identity-crisis", - "reference": "39eb9595c27599391f98ef53ead52300ccfe2901" + "reference": "e7f53dc4d861086ba733fc32571824c19350b9ca" }, "require": { "automattic/jetpack-assets": "@dev", @@ -559,7 +559,7 @@ "link-template": "https://github.com/Automattic/jetpack-identity-crisis/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.13.x-dev" + "dev-trunk": "0.14.x-dev" } }, "autoload": { @@ -1292,7 +1292,7 @@ "dist": { "type": "path", "url": "../../packages/stats", - "reference": "6921a7b466843848f50a84dca877f03e2b3de645" + "reference": "42f8cbf459a7aa8ad59218370eddd4177b1d0316" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1317,7 +1317,7 @@ "link-template": "https://github.com/Automattic/jetpack-stats/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.7.x-dev" + "dev-trunk": "0.8.x-dev" }, "textdomain": "jetpack-stats" }, @@ -1477,7 +1477,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -1485,7 +1485,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/social/CHANGELOG.md b/projects/plugins/social/CHANGELOG.md index 3009dc8f688f0..9c88894982432 100644 --- a/projects/plugins/social/CHANGELOG.md +++ b/projects/plugins/social/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 3.0.0 - 2023-12-06 +### Added +- Added a new post-publish panel for quick sharing [#33231] +- Added Nextdoor to Social Previews [#33907] +- Added traking for social sharing buttons [#33231] + +### Changed +- Code Modernization: Replace usage of strpos() with str_contains() [#34137] +- General: updated PHP requirement to PHP 7.0+ [#34126] +- General: update WordPress version requirements to WordPress 6.3 and compatible with 6.4. [#34127] [#33776] +- Updated package dependencies. +- Updated screenshot to show the new connection toggles. [#33381] +- Updated Social admin pricing page [#33176] + +### Removed +- Removed unused code [#34111] [#34241] + +### Fixed +- Fixed an issue where initial state is not in sync [#33969] +- Fixed broken connections UI [#34391] +- Fixed pre-publish UI reactivity for Jetpack Social [#34243] +- Fixed the issue of publicize remaining ON after the post is published [#34289] + ## 2.3.0 - 2023-09-20 ### Added - Add the change settings logic in Social for the auto conversion feature. [#32712] diff --git a/projects/plugins/social/changelog/add-jetpack-endpoint-for-jetpack-product-data b/projects/plugins/social/changelog/add-jetpack-endpoint-for-jetpack-product-data deleted file mode 100644 index dbda2687742d8..0000000000000 --- a/projects/plugins/social/changelog/add-jetpack-endpoint-for-jetpack-product-data +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: added - - diff --git a/projects/plugins/social/changelog/add-post-publish-one-click-sharing-panel b/projects/plugins/social/changelog/add-post-publish-one-click-sharing-panel deleted file mode 100644 index d0cd5f4ca3a13..0000000000000 --- a/projects/plugins/social/changelog/add-post-publish-one-click-sharing-panel +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: added - -Added a new post-publish panel for quick sharing diff --git a/projects/plugins/social/changelog/add-social-previews-nextdoor b/projects/plugins/social/changelog/add-social-previews-nextdoor deleted file mode 100644 index 50dc4ff569ad9..0000000000000 --- a/projects/plugins/social/changelog/add-social-previews-nextdoor +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: added - -Added Nextdoor to Social Previews diff --git a/projects/plugins/social/changelog/add-tracking-to-post-publish-share-buttons b/projects/plugins/social/changelog/add-tracking-to-post-publish-share-buttons deleted file mode 100644 index 78fd690fb2abd..0000000000000 --- a/projects/plugins/social/changelog/add-tracking-to-post-publish-share-buttons +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: added - -Added traking for social sharing buttons diff --git a/projects/plugins/social/changelog/fix-publicize-remains-on-after-publishing-post b/projects/plugins/social/changelog/fix-publicize-remains-on-after-publishing-post deleted file mode 100644 index 9467c31c0321e..0000000000000 --- a/projects/plugins/social/changelog/fix-publicize-remains-on-after-publishing-post +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fixed - -Fixed the issue of publicize remaining ON after the post is published diff --git a/projects/plugins/social/changelog/fix-social-admin-page b/projects/plugins/social/changelog/fix-social-admin-page deleted file mode 100644 index 011ac1910a4b4..0000000000000 --- a/projects/plugins/social/changelog/fix-social-admin-page +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fixed - -Fixed broken connections UI diff --git a/projects/plugins/social/changelog/fix-social-pre-publish-ui b/projects/plugins/social/changelog/fix-social-pre-publish-ui deleted file mode 100644 index 1bf9e3fe8efe7..0000000000000 --- a/projects/plugins/social/changelog/fix-social-pre-publish-ui +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fixed - -Fixed pre-publish UI reactivity for Jetpack Social diff --git a/projects/plugins/social/changelog/fix-social-store-refresh-issue b/projects/plugins/social/changelog/fix-social-store-refresh-issue deleted file mode 100644 index 31c427e55c6c1..0000000000000 --- a/projects/plugins/social/changelog/fix-social-store-refresh-issue +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fixed - -Fixed an issue where initial state is not in sync diff --git a/projects/plugins/social/changelog/init-release-cycle b/projects/plugins/social/changelog/init-release-cycle index fe0b9fcafb45c..089d4b0d3a9e7 100644 --- a/projects/plugins/social/changelog/init-release-cycle +++ b/projects/plugins/social/changelog/init-release-cycle @@ -1,5 +1,5 @@ Significance: patch Type: changed -Comment: Init 2.3.1-alpha +Comment: Init 3.0.1-alpha diff --git a/projects/plugins/social/changelog/remove-unneeded-condition-from-product-data-api b/projects/plugins/social/changelog/remove-unneeded-condition-from-product-data-api deleted file mode 100644 index b2105c90164a2..0000000000000 --- a/projects/plugins/social/changelog/remove-unneeded-condition-from-product-data-api +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: added -Comment: Just updating lockfile - - diff --git a/projects/plugins/social/changelog/renovate-allure-playwright-2.x b/projects/plugins/social/changelog/renovate-allure-playwright-2.x deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-allure-playwright-2.x +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-babel-monorepo b/projects/plugins/social/changelog/renovate-babel-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-babel-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-babel-monorepo#2 b/projects/plugins/social/changelog/renovate-babel-monorepo#2 deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-babel-monorepo#2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-babel-monorepo#3 b/projects/plugins/social/changelog/renovate-babel-monorepo#3 deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-babel-monorepo#3 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-definitelytyped b/projects/plugins/social/changelog/renovate-definitelytyped deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-definitelytyped +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-js-unit-testing-packages b/projects/plugins/social/changelog/renovate-js-unit-testing-packages deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-js-unit-testing-packages +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-lock-file-maintenance b/projects/plugins/social/changelog/renovate-lock-file-maintenance deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-lock-file-maintenance +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-lock-file-maintenance#3 b/projects/plugins/social/changelog/renovate-lock-file-maintenance#3 deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-lock-file-maintenance#3 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-lock-file-maintenance#4 b/projects/plugins/social/changelog/renovate-lock-file-maintenance#4 deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-lock-file-maintenance#4 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-npm-postcss-vulnerability b/projects/plugins/social/changelog/renovate-npm-postcss-vulnerability deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-npm-postcss-vulnerability +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-playwright-monorepo b/projects/plugins/social/changelog/renovate-playwright-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-playwright-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/jitm-redirection-after-activate-click#2 b/projects/plugins/social/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/jitm-redirection-after-activate-click#2 rename to projects/plugins/social/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/social/changelog/renovate-wordpress-monorepo b/projects/plugins/social/changelog/renovate-wordpress-monorepo deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-wordpress-monorepo +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-wordpress-monorepo#2 b/projects/plugins/social/changelog/renovate-wordpress-monorepo#2 deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-wordpress-monorepo#2 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-wordpress-monorepo#3 b/projects/plugins/social/changelog/renovate-wordpress-monorepo#3 deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-wordpress-monorepo#3 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/renovate-wordpress-monorepo#4 b/projects/plugins/social/changelog/renovate-wordpress-monorepo#4 deleted file mode 100644 index c47cb18e82997..0000000000000 --- a/projects/plugins/social/changelog/renovate-wordpress-monorepo#4 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated package dependencies. diff --git a/projects/plugins/social/changelog/update-changelog-stable-tag b/projects/plugins/social/changelog/update-changelog-stable-tag deleted file mode 100644 index 039174eededad..0000000000000 --- a/projects/plugins/social/changelog/update-changelog-stable-tag +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: added -Comment: Changelog minor edits and stable tag bumps. - - diff --git a/projects/plugins/social/changelog/update-cleanup-unused-jetpack-social-code b/projects/plugins/social/changelog/update-cleanup-unused-jetpack-social-code deleted file mode 100644 index a23c7adbcb925..0000000000000 --- a/projects/plugins/social/changelog/update-cleanup-unused-jetpack-social-code +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: removed - -Removed the unused code diff --git a/projects/plugins/social/changelog/update-convert-e2e-tests-to-esm b/projects/plugins/social/changelog/update-convert-e2e-tests-to-esm deleted file mode 100644 index deda74b2e92f3..0000000000000 --- a/projects/plugins/social/changelog/update-convert-e2e-tests-to-esm +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: changed -Comment: Convert E2E configs to ESM. No change to the plugin itself. - - diff --git a/projects/plugins/social/changelog/update-node-20 b/projects/plugins/social/changelog/update-node-20 deleted file mode 100644 index ef9e5a461a1ce..0000000000000 --- a/projects/plugins/social/changelog/update-node-20 +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: removed -Comment: Remove "engines" from package.json. No change to the project itself; note the build formerly removed this before mirroring. - - diff --git a/projects/plugins/social/changelog/update-php-requirements b/projects/plugins/social/changelog/update-php-requirements deleted file mode 100644 index e91743c9e6518..0000000000000 --- a/projects/plugins/social/changelog/update-php-requirements +++ /dev/null @@ -1,4 +0,0 @@ -Significance: major -Type: changed - -General: updated PHP requirement to PHP 7.0+ diff --git a/projects/plugins/social/changelog/update-refactor-data-request-for-my-jetpack-cards b/projects/plugins/social/changelog/update-refactor-data-request-for-my-jetpack-cards deleted file mode 100644 index 9aa70e3ec1f75..0000000000000 --- a/projects/plugins/social/changelog/update-refactor-data-request-for-my-jetpack-cards +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: changed -Comment: Updated composer.lock. - - diff --git a/projects/plugins/social/changelog/update-remove-jetpack-publicize-store b/projects/plugins/social/changelog/update-remove-jetpack-publicize-store deleted file mode 100644 index 1767a316a1499..0000000000000 --- a/projects/plugins/social/changelog/update-remove-jetpack-publicize-store +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: removed - -Removed jetpack/publicize store diff --git a/projects/plugins/social/changelog/update-social-pricing-page-sig b/projects/plugins/social/changelog/update-social-pricing-page-sig deleted file mode 100644 index 4b8b01c5965ea..0000000000000 --- a/projects/plugins/social/changelog/update-social-pricing-page-sig +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Updated Social admin pricing page diff --git a/projects/plugins/social/changelog/update-social-screenshot b/projects/plugins/social/changelog/update-social-screenshot deleted file mode 100644 index 59922a52cbbb0..0000000000000 --- a/projects/plugins/social/changelog/update-social-screenshot +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: changed - -Updated screenshot to show the new connection toggles. diff --git a/projects/plugins/social/changelog/update-standardize-on-devtool-source-map b/projects/plugins/social/changelog/update-standardize-on-devtool-source-map deleted file mode 100644 index c0bd705cbcf07..0000000000000 --- a/projects/plugins/social/changelog/update-standardize-on-devtool-source-map +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: changed -Comment: Use default devtool from js-packages/webpack-config now that it matches what was being used here already. - - diff --git a/projects/plugins/social/changelog/update-strpos-str-contains b/projects/plugins/social/changelog/update-strpos-str-contains deleted file mode 100644 index f041cf1d01db9..0000000000000 --- a/projects/plugins/social/changelog/update-strpos-str-contains +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Code Modernization: Replace usage of strpos() with str_contains() diff --git a/projects/plugins/social/changelog/update-sync-migrate-before-send-to-before-enqueue b/projects/plugins/social/changelog/update-sync-migrate-before-send-to-before-enqueue deleted file mode 100644 index 9aa70e3ec1f75..0000000000000 --- a/projects/plugins/social/changelog/update-sync-migrate-before-send-to-before-enqueue +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: changed -Comment: Updated composer.lock. - - diff --git a/projects/plugins/social/changelog/update-tested-up-to-64 b/projects/plugins/social/changelog/update-tested-up-to-64 deleted file mode 100644 index 214f1ee53b4ea..0000000000000 --- a/projects/plugins/social/changelog/update-tested-up-to-64 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -General: indicate full compatibility with the latest version of WordPress, 6.4. diff --git a/projects/plugins/social/changelog/update-unpurchased-backup-card-state-my-jetpack b/projects/plugins/social/changelog/update-unpurchased-backup-card-state-my-jetpack deleted file mode 100644 index 7e532c30aafd9..0000000000000 --- a/projects/plugins/social/changelog/update-unpurchased-backup-card-state-my-jetpack +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -Update lockfile diff --git a/projects/plugins/social/changelog/update-vaultpress-backup-card-purchased-state b/projects/plugins/social/changelog/update-vaultpress-backup-card-purchased-state deleted file mode 100644 index 430e5415260af..0000000000000 --- a/projects/plugins/social/changelog/update-vaultpress-backup-card-purchased-state +++ /dev/null @@ -1,5 +0,0 @@ -Significance: patch -Type: changed -Comment: Just a lockfile update - - diff --git a/projects/plugins/social/changelog/update-wp-requirements-63 b/projects/plugins/social/changelog/update-wp-requirements-63 deleted file mode 100644 index d71b22d9e651d..0000000000000 --- a/projects/plugins/social/changelog/update-wp-requirements-63 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: changed - -General: update WordPress version requirements to WordPress 6.3. diff --git a/projects/plugins/social/composer.json b/projects/plugins/social/composer.json index f6b27b2a4bca9..2fd83330c1c0d 100644 --- a/projects/plugins/social/composer.json +++ b/projects/plugins/social/composer.json @@ -81,6 +81,6 @@ "automattic/jetpack-autoloader": true, "automattic/jetpack-composer-plugin": true }, - "autoloader-suffix": "c4802e05bbcf59fd3b6350e8d3e5482c_socialⓥ3_0_0_alpha" + "autoloader-suffix": "c4802e05bbcf59fd3b6350e8d3e5482c_socialⓥ3_0_1_alpha" } } diff --git a/projects/plugins/social/jetpack-social.php b/projects/plugins/social/jetpack-social.php index 33376815d21fa..f6f47d8b60e0f 100644 --- a/projects/plugins/social/jetpack-social.php +++ b/projects/plugins/social/jetpack-social.php @@ -4,7 +4,7 @@ * Plugin Name: Jetpack Social * Plugin URI: https://wordpress.org/plugins/jetpack-social * Description: Share your site’s posts on several social media networks automatically when you publish a new post. - * Version: 3.0.0-alpha + * Version: 3.0.1-alpha * Author: Automattic - Jetpack Social team * Author URI: https://jetpack.com/social/ * License: GPLv2 or later diff --git a/projects/plugins/social/readme.txt b/projects/plugins/social/readme.txt index 208e9b1be49e3..086fe558c2e16 100644 --- a/projects/plugins/social/readme.txt +++ b/projects/plugins/social/readme.txt @@ -100,15 +100,30 @@ The easiest way is to use the Custom Message option in the publishing options bo 4. Manage your Jetpack Social and other Jetpack plugins from My Jetpack. == Changelog == -### 2.3.0 - 2023-09-20 +### 3.0.0 - 2023-12-06 #### Added -- Add the change settings logic in Social for the auto conversion feature. +- Added a new post-publish panel for quick sharing +- Added Nextdoor to Social Previews +- Added traking for social sharing buttons #### Changed -- Changed logic that disables the connections based on the auto-conversion feature. -- General: remove WP 6.1 backwards compatibility checks. -- General: update WordPress version requirements to WordPress 6.2. -- Updated Jetpack submenu sort order so individual features are alpha-sorted. -- Updated package dependencies. [#32803], [#32804], +- Code Modernization: Replace usage of strpos() with str_contains() +- General: updated PHP requirement to PHP 7.0+ +- General: update WordPress version requirements to WordPress 6.3 and compatible with 6.4. [#34127] - Updated package dependencies. +- Updated screenshot to show the new connection toggles. +- Updated Social admin pricing page +#### Removed +- Removed unused code [#34111] + +#### Fixed +- Fixed an issue where initial state is not in sync +- Fixed broken connections UI +- Fixed pre-publish UI reactivity for Jetpack Social +- Fixed the issue of publicize remaining ON after the post is published + +== Upgrade Notice == + += 3.0.0 = +Required for compatibility with Jetpack 12.9 and later. diff --git a/projects/plugins/social/changelog/prerelease b/projects/plugins/starter-plugin/changelog/add-send-secret-url-is-ip similarity index 100% rename from projects/plugins/social/changelog/prerelease rename to projects/plugins/starter-plugin/changelog/add-send-secret-url-is-ip diff --git a/projects/plugins/social/changelog/remove-old-composer-deps b/projects/plugins/starter-plugin/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/remove-old-composer-deps rename to projects/plugins/starter-plugin/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/starter-plugin/composer.lock b/projects/plugins/starter-plugin/composer.lock index 38708bcc65a41..b1c0d2bf4821b 100644 --- a/projects/plugins/starter-plugin/composer.lock +++ b/projects/plugins/starter-plugin/composer.lock @@ -124,7 +124,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "335ee318534ab58327f45abc5da1748a76dd531d" + "reference": "1f713fb83a98dab43f325fe71331d40f9ca47334" }, "require": { "automattic/jetpack-constants": "@dev", @@ -133,7 +133,7 @@ "require-dev": { "automattic/jetpack-changelogger": "@dev", "brain/monkey": "2.6.1", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -529,7 +529,7 @@ "dist": { "type": "path", "url": "../../packages/identity-crisis", - "reference": "39eb9595c27599391f98ef53ead52300ccfe2901" + "reference": "e7f53dc4d861086ba733fc32571824c19350b9ca" }, "require": { "automattic/jetpack-assets": "@dev", @@ -559,7 +559,7 @@ "link-template": "https://github.com/Automattic/jetpack-identity-crisis/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.13.x-dev" + "dev-trunk": "0.14.x-dev" } }, "autoload": { @@ -1379,7 +1379,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -1387,7 +1387,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/social/changelog/renovate-lock-file-maintenance#2 b/projects/plugins/super-cache/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/renovate-lock-file-maintenance#2 rename to projects/plugins/super-cache/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/super-cache/composer.lock b/projects/plugins/super-cache/composer.lock index 306c6de34744a..169b1ce0bab06 100644 --- a/projects/plugins/super-cache/composer.lock +++ b/projects/plugins/super-cache/composer.lock @@ -64,7 +64,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -72,7 +72,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/social/changelog/update-add-bots-sig-to-ua-detection b/projects/plugins/vaultpress/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/update-add-bots-sig-to-ua-detection rename to projects/plugins/vaultpress/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/vaultpress/composer.lock b/projects/plugins/vaultpress/composer.lock index fbf578b4d8058..f41aa6417bed8 100644 --- a/projects/plugins/vaultpress/composer.lock +++ b/projects/plugins/vaultpress/composer.lock @@ -127,7 +127,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -135,7 +135,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/projects/plugins/social/changelog/update-composer-2.6 b/projects/plugins/videopress/changelog/add-send-secret-url-is-ip similarity index 100% rename from projects/plugins/social/changelog/update-composer-2.6 rename to projects/plugins/videopress/changelog/add-send-secret-url-is-ip diff --git a/projects/plugins/social/changelog/update-dedicated-sync-init-hook-priority b/projects/plugins/videopress/changelog/renovate-wikimedia-testing-access-wrapper-3.x similarity index 100% rename from projects/plugins/social/changelog/update-dedicated-sync-init-hook-priority rename to projects/plugins/videopress/changelog/renovate-wikimedia-testing-access-wrapper-3.x diff --git a/projects/plugins/videopress/composer.lock b/projects/plugins/videopress/composer.lock index 4aa6ed0be8fb7..f2aab5fc11a13 100644 --- a/projects/plugins/videopress/composer.lock +++ b/projects/plugins/videopress/composer.lock @@ -124,7 +124,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "335ee318534ab58327f45abc5da1748a76dd531d" + "reference": "1f713fb83a98dab43f325fe71331d40f9ca47334" }, "require": { "automattic/jetpack-constants": "@dev", @@ -133,7 +133,7 @@ "require-dev": { "automattic/jetpack-changelogger": "@dev", "brain/monkey": "2.6.1", - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "suggest": { @@ -529,7 +529,7 @@ "dist": { "type": "path", "url": "../../packages/identity-crisis", - "reference": "39eb9595c27599391f98ef53ead52300ccfe2901" + "reference": "e7f53dc4d861086ba733fc32571824c19350b9ca" }, "require": { "automattic/jetpack-assets": "@dev", @@ -559,7 +559,7 @@ "link-template": "https://github.com/Automattic/jetpack-identity-crisis/compare/v${old}...v${new}" }, "branch-alias": { - "dev-trunk": "0.13.x-dev" + "dev-trunk": "0.14.x-dev" } }, "autoload": { @@ -1410,7 +1410,7 @@ "dist": { "type": "path", "url": "../../packages/changelogger", - "reference": "28b3a05e274c08410b266fa803ed73520d5c2874" + "reference": "a3fe745d83642d741dffe5e1884cc53c65fd056b" }, "require": { "php": ">=7.0", @@ -1418,7 +1418,7 @@ "symfony/process": "^3.4 || ^4.4 || ^5.2 || ^6.0" }, "require-dev": { - "wikimedia/testing-access-wrapper": "^1.0 || ^2.0", + "wikimedia/testing-access-wrapper": "^1.0 || ^2.0 || ^3.0", "yoast/phpunit-polyfills": "1.1.0" }, "bin": [ diff --git a/tools/cli/commands/changelog.js b/tools/cli/commands/changelog.js index 0b0cca3071e6b..be68a0bb4d9e5 100644 --- a/tools/cli/commands/changelog.js +++ b/tools/cli/commands/changelog.js @@ -4,6 +4,7 @@ import path from 'path'; import process from 'process'; import { fileURLToPath } from 'url'; import chalk from 'chalk'; +import enquirer from 'enquirer'; import inquirer from 'inquirer'; import { readComposerJson } from '../helpers/json.js'; import { normalizeProject } from '../helpers/normalizeArgv.js'; @@ -13,7 +14,7 @@ import { runCommand } from '../helpers/runCommand.js'; import { chalkJetpackGreen } from '../helpers/styling.js'; let changeloggerPath = null; - +const { prompt } = enquirer; /** * Comand definition for changelog subcommand. * @@ -340,14 +341,6 @@ async function changelogAdd( argv ) { return; } - console.log( - chalk.yellow( - "When writing your changelog entry, please use the format 'Subject: change description.'\n" + - 'Here is an example of a good changelog entry:\n' + - ' Sitemaps: ensure that the Home URL is slashed on subdirectory websites.\n' - ) - ); - if ( promptConfirm.separateChangelogFiles ) { uniqueProjects.unshift( ...defaultProjects.splice( 0 ) ); } @@ -369,7 +362,7 @@ async function changelogAdd( argv ) { for ( const proj of uniqueProjects ) { console.log( chalk.green( `Running changelogger for ${ proj }!` ) ); - const response = await promptChangelog( argv, proj, projectChangeTypes[ proj ] ); + const response = await promptChangelog( argv, [ proj ], projectChangeTypes[ proj ] ); argv = await formatAutoArgs( proj, argv, response ); await changelogArgs( argv ); } @@ -623,6 +616,7 @@ function doesFilenameExist( fileName, needChangelog ) { fileURLToPath( new URL( './', import.meta.url ) ), `../../../projects/${ proj }/changelog/${ fileName }` ); + try { if ( fs.existsSync( projPath ) ) { console.log( @@ -730,76 +724,108 @@ async function promptChangelog( argv, needChangelog, types ) { .trim() .replace( /\//g, '-' ); const maxLength = Object.keys( types ).reduce( ( a, v ) => ( v.length > a ? v.length : a ), 0 ); - const choices = Object.entries( types ).map( ( [ value, name ] ) => ( { + const typeChoices = Object.entries( types ).map( ( [ value, name ] ) => ( { value, name: `[${ value.padEnd( maxLength, ' ' ) }] ${ name }`, } ) ); - const commands = await inquirer.prompt( [ - { - type: 'string', - name: 'changelogName', - message: 'Name your changelog file:', - default: gitBranch, - validate: input => { - const fileExists = doesFilenameExist( input, needChangelog ); - if ( fileExists ) { - return 'Please choose another file name, or delete the file manually.'; - } - return true; - }, - }, - { - type: 'list', - name: 'significance', - message: 'Significance of the change, in the style of semantic versioning.', - choices: [ - { - value: 'patch', - name: '[patch] Backwards-compatible bug fixes.', - }, - { - value: 'minor', - name: '[minor] Added (or deprecated) functionality in a backwards-compatible manner.', - }, - { - value: 'major', - name: '[major] Broke backwards compatibility in some way.', - }, - ], - }, - { - type: 'list', - name: 'type', - message: 'Type of change.', - choices: choices, - }, - { - type: 'string', - name: 'entry', - message: 'Changelog entry. May be left empty if this change is particularly insignificant.', - when: answers => answers.significance === 'patch', - }, - { - type: 'string', - name: 'comment', - message: - 'You omitted the changelog entry, which is fine. But please comment as to why no entry is needed.', - when: answers => answers.significance === 'patch' && answers.entry === '', + // Get the changelog name. + const { changelogName } = await prompt( { + type: 'input', + name: 'changelogName', + message: 'Name your changelog file:', + default: gitBranch, + validate: input => { + const fileExists = doesFilenameExist( input, needChangelog ); + if ( fileExists ) { + return 'Filename exists already. Please choose another file name, or delete the file existing manually.'; + } + return true; }, - { - type: 'string', + } ); + + // Get the significance. + const { significance } = await prompt( { + type: 'autocomplete', + name: 'significance', + message: 'Significance of the change, in the style of semantic versioning.', + suggest: ( input, choices ) => choices.filter( choice => choice.value.startsWith( input ) ), + highlight: v => v, + choices: [ + { + value: 'patch', + name: '[patch] Backwards-compatible bug fixes.', + }, + { + value: 'minor', + name: '[minor] Added (or deprecated) functionality in a backwards-compatible manner.', + }, + { + value: 'major', + name: '[major] Broke backwards compatibility in some way.', + }, + ], + } ); + + // Get the type of change. + const { type } = await prompt( { + type: 'autocomplete', + name: 'type', + message: 'Type of change.', + suggest: ( input, choices ) => choices.filter( choice => choice.value.startsWith( input ) ), + highlight: v => v, + choices: typeChoices, + } ); + + console.log( + chalk.yellow( + "When writing your changelog entry, please use the format 'Subject: change description.'\n" + + 'Here is an example of a good changelog entry:\n' + + ' Sitemaps: ensure that the Home URL is slashed on subdirectory websites.\n' + ) + ); + + // Get the entry, if it's a patch type it can be left blank. + let entryResponse; + if ( significance !== 'patch' ) { + entryResponse = await prompt( { + type: 'input', name: 'entry', message: 'Changelog entry. May not be empty.', - when: answers => answers.significance === 'minor' || 'major', validate: input => { if ( ! input || ! input.trim() ) { return `Changelog entry can't be blank`; } return true; }, - }, - ] ); - return { ...commands }; + } ); + } else { + entryResponse = await prompt( { + type: 'input', + name: 'entry', + message: 'Changelog entry. May be left empty if this change is particularly insignificant.', + } ); + } + const { entry } = entryResponse; + + // Get the comment if the entry is left blank for a patch level change. + let commentResponse; + if ( entry === '' ) { + commentResponse = await prompt( { + type: 'input', + name: 'comment', + message: + 'You omitted the changelog entry, which is fine. But please comment as to why no entry is needed.', + } ); + } + const { comment } = commentResponse || {}; + + return { + changelogName, + significance, + type, + entry, + comment, + }; } /** diff --git a/tools/cli/package.json b/tools/cli/package.json index 5a592c02e3e52..cfef3939b8b3a 100644 --- a/tools/cli/package.json +++ b/tools/cli/package.json @@ -25,9 +25,9 @@ "@octokit/rest": "19.0.13", "chalk": "4.1.2", "configstore": "5.0.1", + "enquirer": "2.4.1", "envfile": "6.17.0", "execa": "7.0.0", - "tmp": "0.2.1", "glob": "7.1.6", "ignore": "5.1.8", "inquirer": "7.3.3", @@ -42,6 +42,7 @@ "process": "0.11.10", "semver": "7.5.2", "sprintf-js": "1.1.2", + "tmp": "0.2.1", "yargs": "17.6.2" }, "devDependencies": {