Merge tag '5.13.2'
@@ -1,13 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Fetch the versions of WooCommerce from the WordPress API
|
||||
VERSIONS=$(curl -s https://api.wordpress.org/plugins/info/1.0/woocommerce.json | jq -r '.versions | keys_unsorted | .[]' | grep -v 'trunk')
|
||||
LATEST_VERSION=""
|
||||
|
||||
# Find the latest version
|
||||
for version in $VERSIONS; do
|
||||
LATEST_VERSION=$version
|
||||
done
|
||||
LATEST_VERSION=$(
|
||||
curl -s https://api.wordpress.org/plugins/info/1.0/woocommerce.json | \
|
||||
jq -r '.versions | keys_unsorted | .[]' | \
|
||||
grep -v 'trunk' | \
|
||||
sort -V | \
|
||||
tail -n 1
|
||||
)
|
||||
|
||||
# Check if the latest version is a beta/RC version
|
||||
if [[ $LATEST_VERSION != *'beta'* && $LATEST_VERSION != *'rc'* ]]; then
|
||||
|
||||
@@ -197,10 +197,10 @@ jobs:
|
||||
- run:
|
||||
name: Download additional WP Plugins for tests
|
||||
command: |
|
||||
./do download:woo-commerce-zip 9.8.5
|
||||
./do download:woo-commerce-subscriptions-zip 7.5.0
|
||||
./do download:woo-commerce-zip 10.0.4
|
||||
./do download:woo-commerce-subscriptions-zip 7.7.0
|
||||
./do download:woo-commerce-memberships-zip
|
||||
./do download:automate-woo-zip 6.1.13
|
||||
./do download:automate-woo-zip 6.1.15
|
||||
- run:
|
||||
name: Dump tests ENV variables for acceptance tests
|
||||
command: |
|
||||
@@ -468,7 +468,7 @@ jobs:
|
||||
name: Download WooCommerce Core
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_acceptance
|
||||
- when:
|
||||
condition: << parameters.woo_subscriptions_version >>
|
||||
steps:
|
||||
@@ -476,7 +476,7 @@ jobs:
|
||||
name: Download WooCommerce Subscriptions
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_acceptance
|
||||
- when:
|
||||
condition: << parameters.automate_woo_version >>
|
||||
steps:
|
||||
@@ -484,7 +484,7 @@ jobs:
|
||||
name: Download AutomateWoo
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_acceptance
|
||||
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_acceptance
|
||||
- run:
|
||||
name: Group acceptance tests
|
||||
command: |
|
||||
@@ -753,7 +753,7 @@ jobs:
|
||||
name: Download WooCommerce Core
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-zip ${WOOCOMMERCE_VERSION}" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_integration
|
||||
- when:
|
||||
condition: << parameters.woo_subscriptions_version >>
|
||||
steps:
|
||||
@@ -761,7 +761,7 @@ jobs:
|
||||
name: Download WooCommerce Subscriptions
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
docker compose run --rm -w /project --entrypoint "./do download:woo-commerce-subscriptions-zip << parameters.woo_subscriptions_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_integration
|
||||
- when:
|
||||
condition: << parameters.automate_woo_version >>
|
||||
steps:
|
||||
@@ -769,7 +769,7 @@ jobs:
|
||||
name: Download AutomateWoo
|
||||
command: |
|
||||
cd ../tests_env/docker
|
||||
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_TOKEN=${WP_GITHUB_TOKEN} codeception_integration
|
||||
docker compose run --rm -w /project --entrypoint "./do download:automate-woo-zip << parameters.automate_woo_version >>" --no-deps -e WP_GITHUB_USERNAME=${WP_GITHUB_USERNAME} -e WP_GITHUB_WOOCOMMERCE_TOKEN=${WP_GITHUB_WOOCOMMERCE_TOKEN} codeception_integration
|
||||
- run:
|
||||
name: 'PHP Integration tests'
|
||||
command: |
|
||||
@@ -1182,11 +1182,11 @@ workflows:
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: acceptance_oldest
|
||||
woo_core_version: 9.7.1
|
||||
woo_subscriptions_version: 7.4.0
|
||||
woo_core_version: 9.9.5
|
||||
woo_subscriptions_version: 7.6.0
|
||||
automate_woo_version: 6.0.33
|
||||
mysql_command: --max_allowed_packet=100M
|
||||
mysql_image: mysql:5.5
|
||||
mysql_image: mysql:5.6
|
||||
codeception_image_version: 7.4-cli_20220605.0
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.7.2
|
||||
@@ -1222,14 +1222,14 @@ workflows:
|
||||
- integration_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: integration_oldest
|
||||
woo_core_version: 9.7.1
|
||||
woo_subscriptions_version: 7.4.0
|
||||
woo_core_version: 9.9.5
|
||||
woo_subscriptions_version: 7.6.0
|
||||
automate_woo_version: 6.0.33
|
||||
codeception_image_version: 7.4-cli_20220605.0
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI # We use image with PHP 7.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.7.2
|
||||
mysql_command: --max_allowed_packet=100M
|
||||
mysql_image: mysql:5.5
|
||||
mysql_image: mysql:5.6
|
||||
requires:
|
||||
- build
|
||||
- build_premium:
|
||||
@@ -1284,8 +1284,8 @@ workflows:
|
||||
- acceptance_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: acceptance_with_premium_oldest
|
||||
woo_core_version: 9.7.1
|
||||
woo_subscriptions_version: 7.4.0
|
||||
woo_core_version: 9.9.5
|
||||
woo_subscriptions_version: 7.6.0
|
||||
automate_woo_version: 6.0.33
|
||||
codeception_image_version: 7.4-cli_20220605.0
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
|
||||
@@ -1295,14 +1295,14 @@ workflows:
|
||||
- integration_tests:
|
||||
<<: *slack-fail-post-step
|
||||
name: integration_with_premium_oldest
|
||||
woo_core_version: 9.7.1
|
||||
woo_subscriptions_version: 7.4.0
|
||||
woo_core_version: 9.9.5
|
||||
woo_subscriptions_version: 7.6.0
|
||||
automate_woo_version: 6.0.33
|
||||
codeception_image_version: 7.4-cli_20220605.0
|
||||
wordpress_image_version: 6.1.1-php7.4 # We use image with PHP 7.4 and install required WordPress version via CLI
|
||||
wordpress_version: 6.7.2
|
||||
mysql_command: --max_allowed_packet=100M
|
||||
mysql_image: mysql:5.5
|
||||
mysql_image: mysql:5.6
|
||||
requires:
|
||||
- build_premium
|
||||
|
||||
|
||||
2
.github/pull_request_template.md
vendored
@@ -24,7 +24,7 @@ _N/A_
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] I have added a changelog entry (pcNwfB-4Ov-p2#how-to-write-a-changelog)
|
||||
- [ ] I have added a changelog entry (`./do changelog:add --type=<type> --description=<description>`)
|
||||
- [ ] I followed [best practices](https://codex.wordpress.org/I18n_for_WordPress_Developers) for translations
|
||||
- [ ] I added sufficient test coverage
|
||||
- [ ] I embraced TypeScript by either creating new files in TypeScript or converting existing JavaScript files when making changes
|
||||
|
||||
17
.github/workflows/scripts/helpers.php
vendored
@@ -118,10 +118,21 @@ function replacePrivatePluginVersion(
|
||||
string $configParameterName,
|
||||
string $versionsFilename
|
||||
): void {
|
||||
// Read the GitHub token from environment variable
|
||||
$token = getenv('GH_TOKEN');
|
||||
// Read the GitHub token from environment variable. Set at https://github.com/mailpoet/mailpoet/settings/secrets/actions.
|
||||
|
||||
// If the repository is woocommerce, use the WooCommerce token, otherwise use the general token.
|
||||
$isWooCommerceRepository = strpos($repository, 'woocommerce/') !== false;
|
||||
if ($isWooCommerceRepository) {
|
||||
$token = getenv('WP_GITHUB_WOOCOMMERCE_TOKEN');
|
||||
} else {
|
||||
$token = getenv('WP_GITHUB_TOKEN');
|
||||
}
|
||||
if (!$token) {
|
||||
die("GitHub token not found. Make sure it's set in the environment variable 'GH_TOKEN'.");
|
||||
if ($isWooCommerceRepository) {
|
||||
die("WooCommerce token not found. For WooCommerce repositories requests, make sure to set the token in the environment variable 'WP_GITHUB_WOOCOMMERCE_TOKEN'.");
|
||||
} else {
|
||||
die("GitHub token not found. Make sure it's set in the environment variable 'WP_GITHUB_TOKEN'.");
|
||||
}
|
||||
}
|
||||
|
||||
$page = 1;
|
||||
|
||||
46
dev/php84/Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
FROM wordpress:php8.4-apache
|
||||
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
|
||||
# additinal extensions
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y git zlib1g-dev libzip-dev zip wget gnupg msmtp libpng-dev gettext subversion \
|
||||
&& \
|
||||
# Install NodeJS, enable Corepack
|
||||
curl -sL https://deb.nodesource.com/setup_19.x | bash - && \
|
||||
apt-get install -y nodejs build-essential && \
|
||||
corepack enable && \
|
||||
\
|
||||
# Install WP-CLI
|
||||
curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
|
||||
chmod +x /usr/local/bin/wp && \
|
||||
\
|
||||
# Clean up
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY dev/php.ini /usr/local/etc/php/conf.d/php_user.ini
|
||||
|
||||
# msmtp config
|
||||
RUN printf "account default\nhost smtp\nport 1025" > /etc/msmtprc
|
||||
|
||||
# xdebug build an config
|
||||
ENV XDEBUGINI_PATH=/usr/local/etc/php/conf.d/xdebug.ini
|
||||
RUN git clone -b "3.4.5" --depth 1 https://github.com/xdebug/xdebug.git /usr/src/php/ext/xdebug \
|
||||
&& docker-php-ext-configure xdebug --enable-xdebug-dev \
|
||||
&& docker-php-ext-install xdebug \
|
||||
&& mkdir /tmp/debug
|
||||
COPY dev/xdebug.ini /tmp/xdebug.ini
|
||||
RUN cat /tmp/xdebug.ini >> $XDEBUGINI_PATH
|
||||
|
||||
# php extensions
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
RUN docker-php-ext-install mysqli
|
||||
|
||||
# allow .htaccess files (between <Directory /var/www/> and </Directory>, which is WordPress installation)
|
||||
RUN sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
|
||||
|
||||
# ensure existing content in /var/www/html respects UID and GID, give Node permissions for Corepack
|
||||
RUN chown -R ${UID}:${GID} /var/www/html && \
|
||||
mkdir -p /.node && chown -R ${UID}:${GID} /.node
|
||||
@@ -34,6 +34,9 @@ WP_CIRCLECI_TOKEN=
|
||||
WP_GITHUB_USERNAME=
|
||||
WP_GITHUB_TOKEN=
|
||||
|
||||
# GitHub fine-grained token for the WooCommerce organization. Fetch it from the Secret Store by searching for "MailPoet: GitHub MailPoet CI - WooCommerce token".
|
||||
WP_GITHUB_WOOCOMMERCE_TOKEN=
|
||||
|
||||
# k6 performance test suite
|
||||
# Get following secrets from the Secret Store, look for "MailPoet: plugin .env":
|
||||
WP_TEST_PERFORMANCE_DATA_URL=
|
||||
|
||||
44
mailpoet/CHANGELOG.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Changelog System
|
||||
|
||||
The `changelog/` directory contains individual changelog entries that are compiled into the final changelog during releases.
|
||||
|
||||
## How it works
|
||||
|
||||
Create individual changelog files in this directory. Each file represents a single changelog entry.
|
||||
|
||||
## File naming convention
|
||||
|
||||
Files should be named using the following pattern:
|
||||
|
||||
- `YYYY-MM-DD-HH-MM-SS-{type}-{description}.md`
|
||||
|
||||
Examples:
|
||||
|
||||
- `2024-01-15-14-30-00-fix-undefined-array-key.md`
|
||||
- `2024-01-15-14-35-00-improve-polylang-support.md`
|
||||
- `2024-01-15-14-40-00-update-woocommerce-segments.md`
|
||||
|
||||
## File format
|
||||
|
||||
Each changelog file should contain:
|
||||
|
||||
```markdown
|
||||
# Type: {Added|Improved|Fixed|Changed|Updated|Removed}
|
||||
|
||||
# Description
|
||||
|
||||
Brief description of the change
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
- `Added`: New features
|
||||
- `Improved`: Enhancements to existing features
|
||||
- `Fixed`: Bug fixes
|
||||
- `Changed`: Changes to existing functionality
|
||||
- `Updated`: Updates to dependencies, requirements, etc.
|
||||
- `Removed`: Removed features or functionality
|
||||
|
||||
## Compilation
|
||||
|
||||
During the release process, these individual files are compiled into the final changelog format used in `readme.txt` and `changelog.txt`.
|
||||
@@ -9,8 +9,9 @@
|
||||
4. [Coding and Testing](#coding-and-testing)
|
||||
1. [DI](#di)
|
||||
2. [PHP-Scoper](#php-scoper)
|
||||
3. [i18n](#i18n)
|
||||
4. [Acceptance testing](#acceptance-testing)
|
||||
3. [Changelog](#changelog)
|
||||
4. [i18n](#i18n)
|
||||
5. [Acceptance testing](#acceptance-testing)
|
||||
|
||||
## MailPoet
|
||||
|
||||
@@ -121,6 +122,8 @@ $ ./do qa # PHP and JS linters.
|
||||
|
||||
$ ./do release:changelog-get [--version-name=...] # Prints out changelog and release notes for given version or for newest version.
|
||||
$ ./do release:changelog-update [--version-name=...] [--quiet] # Updates changelog in readme.txt for given version or for newest version.
|
||||
$ ./do changelog:add --type=<type> --description=<description> # Creates a new changelog entry
|
||||
$ ./do changelog:preview [--version=<version>] # Preview compiled changelog for next version
|
||||
|
||||
$ ./do container:dump # Generates DI container cache.
|
||||
|
||||
@@ -140,6 +143,16 @@ You can check [the docs](https://symfony.com/doc/3.4/components/dependency_injec
|
||||
We use PHP-Scoper package to prevent plugin libraries conflicts in PHP. Two plugins may be using different versions of a library. PHP-Scoper prefix dependencies namespaces and they are then moved into `vendor-prefixed` directory.
|
||||
Dependencies handled by PHP-Scoper are configured in extra configuration files `prefixer/composer.json` and `prefixer/scoper.inc.php`. Installation and processing is triggered in post scripts of the main `composer.json` file.
|
||||
|
||||
### Changelog
|
||||
|
||||
Create changelog entries using:
|
||||
|
||||
```bash
|
||||
./do changelog:add --type=Fixed --description="Brief description of the change"
|
||||
```
|
||||
|
||||
See [readme](changelog/README.md) for detailed documentation.
|
||||
|
||||
### i18n
|
||||
|
||||
We use functions `__()`, `_n()`, `_x()`, and `_nx()` with domain `mailpoet` to translate strings. Please follow [best practices](https://codex.wordpress.org/I18n_for_WordPress_Developers).
|
||||
|
||||
@@ -523,7 +523,9 @@ class RoboFile extends \Robo\Tasks {
|
||||
}
|
||||
|
||||
public function containerDump() {
|
||||
define('ABSPATH', getenv('WP_ROOT') . '/');
|
||||
if (!defined('ABSPATH')) {
|
||||
define('ABSPATH', getenv('WP_ROOT') . '/');
|
||||
}
|
||||
if (!file_exists(ABSPATH . 'wp-config.php')) {
|
||||
$this->yell('WP_ROOT env variable does not contain valid path to wordpress root.', 40, 'red');
|
||||
exit(1);
|
||||
@@ -1209,6 +1211,39 @@ class RoboFile extends \Robo\Tasks {
|
||||
$this->say("Changelog \n{$changelog}");
|
||||
}
|
||||
|
||||
public function changelogAdd($opts = ['type' => '', 'description' => '']) {
|
||||
$type = $opts['type'];
|
||||
$description = $opts['description'];
|
||||
|
||||
if (empty($type)) {
|
||||
$this->say('Please specify a type with --type=<type>');
|
||||
$this->say('Valid types: Added, Improved, Fixed, Changed, Updated, Removed');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (empty($description)) {
|
||||
$this->say('Please specify a description with --description=<description>');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$changelogger = new \MailPoetTasks\Release\Changelogger();
|
||||
$filePath = $changelogger->createChangelogEntry($type, $description);
|
||||
|
||||
$this->say("Changelog entry created: $filePath");
|
||||
}
|
||||
|
||||
public function changelogPreview($version = null) {
|
||||
if (!$version) {
|
||||
$version = $this->releaseVersionGetNext($version);
|
||||
}
|
||||
|
||||
$changelogger = new \MailPoetTasks\Release\Changelogger();
|
||||
$changelog = $changelogger->compileChangelog($version);
|
||||
|
||||
$this->say("Preview changelog for version $version:");
|
||||
$this->say($changelog);
|
||||
}
|
||||
|
||||
public function releaseVerifyDownloadedZip($version) {
|
||||
$this->say('Verifying ZIP file');
|
||||
$zip = new ZipArchive();
|
||||
@@ -1276,29 +1311,33 @@ class RoboFile extends \Robo\Tasks {
|
||||
}
|
||||
|
||||
public function downloadWooCommerceMembershipsZip(): void {
|
||||
if (!getenv('WP_GITHUB_USERNAME') && !getenv('WP_GITHUB_TOKEN')) {
|
||||
$token = getenv('WP_GITHUB_WOOCOMMERCE_TOKEN');
|
||||
if (!getenv('WP_GITHUB_USERNAME') && !$token) {
|
||||
$this->yell("Skipping download of WooCommerce Memberships", 40, 'red');
|
||||
exit(0); // Exit with 0 since it is a valid state for some environments
|
||||
}
|
||||
$this->createGithubClient('woocommerce/all-plugins')
|
||||
$this->createGithubClient('woocommerce/all-plugins', $token)
|
||||
->downloadRawFile('https://api.github.com/repos/woocommerce/all-plugins/contents/product-packages/woocommerce-memberships/woocommerce-memberships.zip?ref=master', 'woocommerce-memberships.zip', __DIR__ . '/tests/plugins/');
|
||||
}
|
||||
|
||||
public function downloadWooCommerceSubscriptionsZip($tag = null) {
|
||||
if (!getenv('WP_GITHUB_USERNAME') && !getenv('WP_GITHUB_TOKEN')) {
|
||||
$token = getenv('WP_GITHUB_WOOCOMMERCE_TOKEN');
|
||||
if (!getenv('WP_GITHUB_USERNAME') && !$token) {
|
||||
$this->yell("Skipping download of WooCommerce Subscriptions", 40, 'red');
|
||||
exit(0); // Exit with 0 since it is a valid state for some environments
|
||||
}
|
||||
$this->createGithubClient('woocommerce/woocommerce-subscriptions')
|
||||
|
||||
$this->createGithubClient('woocommerce/woocommerce-subscriptions', $token)
|
||||
->downloadReleaseZip('woocommerce-subscriptions.zip', __DIR__ . '/tests/plugins/', $tag);
|
||||
}
|
||||
|
||||
public function downloadAutomateWooZip($tag = null) {
|
||||
if (!getenv('WP_GITHUB_USERNAME') && !getenv('WP_GITHUB_TOKEN')) {
|
||||
$token = getenv('WP_GITHUB_WOOCOMMERCE_TOKEN');
|
||||
if (!getenv('WP_GITHUB_USERNAME') && !$token) {
|
||||
$this->yell("Skipping download of Automate Woo", 40, 'red');
|
||||
exit(0); // Exit with 0 since it is a valid state for some environments
|
||||
}
|
||||
$this->createGithubClient('woocommerce/automatewoo')
|
||||
$this->createGithubClient('woocommerce/automatewoo', $token)
|
||||
->downloadReleaseZip('automatewoo.zip', __DIR__ . '/tests/plugins/', $tag);
|
||||
}
|
||||
|
||||
@@ -1454,11 +1493,12 @@ class RoboFile extends \Robo\Tasks {
|
||||
);
|
||||
}
|
||||
|
||||
protected function createGitHubController($project = \MailPoetTasks\Release\GitHubController::PROJECT_MAILPOET) {
|
||||
protected function createGitHubController($project = \MailPoetTasks\Release\GitHubController::PROJECT_MAILPOET, $token = null) {
|
||||
$help = "Use your GitHub username and a token from https://github.com/settings/tokens with 'repo' scopes.";
|
||||
$token = $token ?: $this->getEnv('WP_GITHUB_TOKEN', $help);
|
||||
return new \MailPoetTasks\Release\GitHubController(
|
||||
$this->getEnv('WP_GITHUB_USERNAME', $help),
|
||||
$this->getEnv('WP_GITHUB_TOKEN', $help),
|
||||
$token,
|
||||
$project
|
||||
);
|
||||
}
|
||||
@@ -1495,12 +1535,13 @@ class RoboFile extends \Robo\Tasks {
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
private function createGithubClient($repositoryName) {
|
||||
private function createGithubClient($repositoryName, $token = null) {
|
||||
require_once __DIR__ . '/tasks/GithubClient.php';
|
||||
$token = $token ?: $this->getEnv('WP_GITHUB_TOKEN');
|
||||
return new \MailPoetTasks\GithubClient(
|
||||
$repositoryName,
|
||||
getenv('WP_GITHUB_USERNAME') ?: null,
|
||||
getenv('WP_GITHUB_TOKEN') ?: null
|
||||
$token
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1510,7 +1551,9 @@ class RoboFile extends \Robo\Tasks {
|
||||
}
|
||||
|
||||
private function createDoctrineEntityManager() {
|
||||
define('ABSPATH', getenv('WP_ROOT') . '/');
|
||||
if (!defined('ABSPATH')) {
|
||||
define('ABSPATH', getenv('WP_ROOT') . '/');
|
||||
}
|
||||
if (\MailPoet\Config\Env::$dbPrefix === null) {
|
||||
/**
|
||||
* Ensure some prefix is set
|
||||
|
||||
@@ -21,11 +21,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet_social_icons_container {
|
||||
> input {
|
||||
vertical-align: top;
|
||||
|
||||
&:checked
|
||||
~ #mailpoet_social_icons_styles
|
||||
.mailpoet_social_icon_set:not([data-setname^='official']) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> label {
|
||||
display: inline-block;
|
||||
margin: -0.25rem 0 0.5rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet_social_icon_set {
|
||||
border: 1px solid transparent;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px;
|
||||
|
||||
&:not([data-setname^='official']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $editor-social-icon-border-color-hover;
|
||||
}
|
||||
|
||||
@@ -243,3 +243,9 @@ progress::-moz-progress-bar {
|
||||
.mailpoet-form-field-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mailpoet-password-input-toggle {
|
||||
border-radius: 0 3px 3px 0 !important;
|
||||
height: 100%;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
@@ -96,3 +96,12 @@ tr {
|
||||
font-size: $font-size;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet-notice-container {
|
||||
.mailpoet_notice {
|
||||
box-sizing: border-box;
|
||||
margin: 0 0 1em;
|
||||
max-width: $grid-column;
|
||||
padding-right: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,31 +62,6 @@
|
||||
background: $color-badge-green;
|
||||
}
|
||||
|
||||
.mailpoet_badge_video {
|
||||
background: $color-badge-video-guide;
|
||||
display: inline-block;
|
||||
line-height: 20px;
|
||||
padding: 3px 6px;
|
||||
text-decoration: none;
|
||||
vertical-align: top;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: $color-badge-green;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dashicons {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.mailpoet_badge_video_grey {
|
||||
background: #c3c3c3;
|
||||
}
|
||||
|
||||
// h1.title needed to override WP styles specificity
|
||||
h1.title.mailpoet-newsletter-listing-heading {
|
||||
margin-bottom: $grid-gap;
|
||||
|
||||
@@ -165,8 +165,3 @@ ul.sending-method-benefits {
|
||||
.mailpoet-verify-key-button {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.mailpoet-premium-key-toggle {
|
||||
height: 34px;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
border: 1px solid $color-input-border !important;
|
||||
border-radius: $form-control-border-radius !important;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
min-height: $form-control-height;
|
||||
min-width: 0;
|
||||
width: $grid-column;
|
||||
// To align the left padding with the other inputs
|
||||
input[type='text'].components-form-token-field__input {
|
||||
line-height: 16px;
|
||||
margin-left: 0;
|
||||
min-height: 30px;
|
||||
min-height: 24px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
// For better fit when the last item is active
|
||||
|
||||
|
After Width: | Height: | Size: 451 B |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 500 B |
|
After Width: | Height: | Size: 366 B |
|
After Width: | Height: | Size: 474 B |
|
After Width: | Height: | Size: 517 B |
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 553 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 543 B |
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 361 B |
|
After Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 487 B |
|
After Width: | Height: | Size: 553 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 604 B |
|
After Width: | Height: | Size: 583 B |
|
After Width: | Height: | Size: 274 B |
|
After Width: | Height: | Size: 307 B |
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 485 B |
|
After Width: | Height: | Size: 384 B |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 490 B |
|
After Width: | Height: | Size: 330 B |
|
After Width: | Height: | Size: 451 B |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 500 B |
|
After Width: | Height: | Size: 366 B |
|
After Width: | Height: | Size: 474 B |
|
After Width: | Height: | Size: 517 B |
|
After Width: | Height: | Size: 561 B |
|
After Width: | Height: | Size: 502 B |
|
After Width: | Height: | Size: 553 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 543 B |
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 361 B |
|
After Width: | Height: | Size: 550 B |
|
After Width: | Height: | Size: 487 B |
|
After Width: | Height: | Size: 553 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 604 B |
|
After Width: | Height: | Size: 584 B |
|
After Width: | Height: | Size: 274 B |
|
After Width: | Height: | Size: 307 B |
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 486 B |
|
After Width: | Height: | Size: 384 B |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 490 B |
|
After Width: | Height: | Size: 330 B |
@@ -1,6 +1,6 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { DropdownMenu } from '@wordpress/components';
|
||||
import { moreVertical, trash } from '@wordpress/icons';
|
||||
import { moreVertical, trash, copy } from '@wordpress/icons';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Hooks } from 'wp-js-hooks';
|
||||
import { PremiumModal } from 'common/premium-modal';
|
||||
@@ -15,10 +15,43 @@ type Props = {
|
||||
export function StepMoreMenu({ step }: Props): JSX.Element {
|
||||
const { context } = useContext(AutomationContext);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showDuplicateModal, setShowDuplicateModal] = useState(false);
|
||||
|
||||
const canDuplicate = step.key !== 'core:if-else' && step.type !== 'trigger';
|
||||
const moreControls: StepMoreControlsType = Hooks.applyFilters(
|
||||
'mailpoet.automation.step.more-controls',
|
||||
{
|
||||
...(canDuplicate && {
|
||||
duplicate: {
|
||||
key: 'duplicate',
|
||||
control: {
|
||||
title: __('Duplicate step', 'mailpoet'),
|
||||
icon: copy,
|
||||
onClick: () => setShowDuplicateModal(true),
|
||||
},
|
||||
slot: () => {
|
||||
if (!showDuplicateModal) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
<PremiumModal
|
||||
onRequestClose={() => {
|
||||
setShowDuplicateModal(false);
|
||||
}}
|
||||
tracking={{
|
||||
utm_medium: 'upsell_modal',
|
||||
utm_campaign: 'duplicate_automation_step',
|
||||
}}
|
||||
>
|
||||
{__(
|
||||
'Duplicating automation steps is available in premium plans. Upgrade to unlock this feature.',
|
||||
'mailpoet',
|
||||
)}
|
||||
</PremiumModal>
|
||||
);
|
||||
},
|
||||
},
|
||||
}),
|
||||
delete: {
|
||||
key: 'delete',
|
||||
control: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { plus } from '@wordpress/icons';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { store as noticesStore } from '@wordpress/notices';
|
||||
import { Button } from '../../../components/button';
|
||||
import { storeName } from '../../../../../editor/store';
|
||||
import { MailPoet } from '../../../../../../mailpoet';
|
||||
@@ -30,6 +31,8 @@ export function EditNewsletter(): JSX.Element {
|
||||
const [redirectToTemplateSelection, setRedirectToTemplateSelection] =
|
||||
useState(false);
|
||||
const [fetchingPreviewLink, setFetchingPreviewLink] = useState(false);
|
||||
const [isHandlingDuplicatedStep, setIsHandlingDuplicatedStep] =
|
||||
useState(false);
|
||||
|
||||
const { selectedStep, automationId, savedState, errors } = useSelect(
|
||||
(select) => ({
|
||||
@@ -47,6 +50,7 @@ export function EditNewsletter(): JSX.Element {
|
||||
const automationStepId = selectedStep.id;
|
||||
const errorFields = errors?.fields ?? {};
|
||||
const emailIdError = errorFields?.email_id ?? '';
|
||||
const isDuplicatedStep = selectedStep?.args?.stepDuplicated === true;
|
||||
|
||||
const createEmail = useCallback(async () => {
|
||||
setRedirectToTemplateSelection(true);
|
||||
@@ -74,6 +78,61 @@ export function EditNewsletter(): JSX.Element {
|
||||
void dispatch(storeName).save();
|
||||
}, [automationId, automationStepId]);
|
||||
|
||||
const handleDuplicatedStep = useCallback(async (): Promise<number | null> => {
|
||||
try {
|
||||
// Save the automation to trigger backend duplication
|
||||
const savedData = await dispatch(storeName).save();
|
||||
const newSelectedStep = savedData.automation.steps[automationStepId];
|
||||
const newEmailId = Number(newSelectedStep?.args?.email_id);
|
||||
|
||||
if (newEmailId && !Number.isNaN(newEmailId)) {
|
||||
return newEmailId;
|
||||
}
|
||||
|
||||
throw new Error('Failed to retrieve new email ID after duplication');
|
||||
} catch (error) {
|
||||
void dispatch(noticesStore).createErrorNotice(
|
||||
__('Email duplication failed. Please try again.', 'mailpoet'),
|
||||
{ explicitDismiss: true },
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}, [automationStepId]);
|
||||
|
||||
const handleEditContent = useCallback(async () => {
|
||||
// Ensure we have a valid selected step
|
||||
if (!selectedStep?.args?.email_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure email ID is a valid number to prevent injection
|
||||
const currentEmailId = Number(selectedStep.args.email_id);
|
||||
if (!currentEmailId || Number.isNaN(currentEmailId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newUrl = `?page=mailpoet-newsletter-editor&id=${currentEmailId}&context=automation`;
|
||||
|
||||
if (isDuplicatedStep) {
|
||||
setIsHandlingDuplicatedStep(true);
|
||||
|
||||
const newEmailId = await handleDuplicatedStep();
|
||||
|
||||
if (newEmailId) {
|
||||
newUrl = `?page=mailpoet-newsletter-editor&id=${newEmailId}&context=automation`;
|
||||
} else {
|
||||
// If duplication failed, don't redirect and let user see the error
|
||||
setIsHandlingDuplicatedStep(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsHandlingDuplicatedStep(false);
|
||||
}
|
||||
|
||||
window.location.href = newUrl;
|
||||
}, [isDuplicatedStep, selectedStep?.args?.email_id, handleDuplicatedStep]);
|
||||
|
||||
// This component is rendered only when no email ID is set. Once we have the ID
|
||||
// and the automation is saved, we can safely redirect to the email design flow.
|
||||
useEffect(() => {
|
||||
@@ -112,9 +171,9 @@ export function EditNewsletter(): JSX.Element {
|
||||
<Button
|
||||
variant="sidebar-primary"
|
||||
centered
|
||||
href={`?page=mailpoet-newsletter-editor&id=${
|
||||
selectedStep.args.email_id as string
|
||||
}&context=automation`}
|
||||
onClick={handleEditContent}
|
||||
isBusy={isHandlingDuplicatedStep}
|
||||
disabled={isHandlingDuplicatedStep}
|
||||
>
|
||||
{__('Edit content', 'mailpoet')}
|
||||
</Button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { step as OrderStatusChanged } from './steps/order-status-changed';
|
||||
import { step as OrderCompletedTrigger } from './steps/order-completed';
|
||||
import { step as OrderCancelledTrigger } from './steps/order-cancelled';
|
||||
import { step as OrderCreatedTrigger } from './steps/order-created';
|
||||
import { step as OrderNoteAddedTrigger } from './steps/order-note-added';
|
||||
import { step as AbandonedCartTrigger } from './steps/abandoned-cart';
|
||||
import { MailPoet } from '../../../mailpoet';
|
||||
import { step as BuysAProductTrigger } from './steps/buys-a-product';
|
||||
@@ -19,6 +20,7 @@ export const initialize = (): void => {
|
||||
registerStepType(OrderCompletedTrigger);
|
||||
registerStepType(OrderCancelledTrigger);
|
||||
registerStepType(OrderCreatedTrigger);
|
||||
registerStepType(OrderNoteAddedTrigger);
|
||||
registerStepType(AbandonedCartTrigger);
|
||||
registerStepType(BuysAProductTrigger);
|
||||
registerStepType(BuysFromACategory);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { dispatch, useSelect } from '@wordpress/data';
|
||||
import { PanelBody, SelectControl, TextControl } from '@wordpress/components';
|
||||
import { PlainBodyTitle } from '../../../../../editor/components';
|
||||
import { storeName } from '../../../../../editor/store';
|
||||
|
||||
export function Edit(): JSX.Element {
|
||||
const { selectedStep } = useSelect(
|
||||
(select) => ({
|
||||
selectedStep: select(storeName).getSelectedStep(),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const noteType = (selectedStep.args?.note_type as string) ?? 'all';
|
||||
const noteContains = (selectedStep.args?.note_contains as string) ?? '';
|
||||
|
||||
return (
|
||||
<PanelBody opened>
|
||||
<PlainBodyTitle title={__('Trigger settings', 'mailpoet')} />
|
||||
|
||||
<SelectControl
|
||||
label={__('Note type', 'mailpoet')}
|
||||
value={noteType}
|
||||
options={[
|
||||
{ label: __('All', 'mailpoet'), value: 'all' },
|
||||
{ label: __('Note to customer', 'mailpoet'), value: 'customer' },
|
||||
{ label: __('Private note', 'mailpoet'), value: 'private' },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
void dispatch(storeName).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'note_type',
|
||||
value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextControl
|
||||
label={__('Note contains text', 'mailpoet')}
|
||||
help={__(
|
||||
'Only trigger this workflow if the order note contains the certain text. This field is optional.',
|
||||
'mailpoet',
|
||||
)}
|
||||
value={noteContains}
|
||||
onChange={(value) => {
|
||||
void dispatch(storeName).updateStepArgs(
|
||||
selectedStep.id,
|
||||
'note_contains',
|
||||
value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PanelBody>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { __, _x } from '@wordpress/i18n';
|
||||
import { commentContent } from '@wordpress/icons';
|
||||
import { StepType } from '../../../../editor/store';
|
||||
import { Edit } from './edit';
|
||||
|
||||
const keywords = [
|
||||
__('woocommerce', 'mailpoet'),
|
||||
// translators: noun, used as a search keyword for "Order note added" trigger
|
||||
__('order', 'mailpoet'),
|
||||
// translators: noun, used as a search keyword for "Order note added" trigger
|
||||
__('note', 'mailpoet'),
|
||||
// translators: noun, used as a search keyword for "Order note added" trigger
|
||||
__('comment', 'mailpoet'),
|
||||
];
|
||||
|
||||
export const step: StepType = {
|
||||
key: 'woocommerce:order-note-added',
|
||||
group: 'triggers',
|
||||
title: () => __('Order note added', 'mailpoet'),
|
||||
description: () =>
|
||||
__(
|
||||
'Fires when any note is added to an order, can include both private notes and notes to the customer. These notes appear on the right of the order edit screen.',
|
||||
'mailpoet',
|
||||
),
|
||||
subtitle: () => _x('Trigger', 'noun', 'mailpoet'),
|
||||
keywords,
|
||||
foreground: '#2271b1',
|
||||
background: '#f0f6fc',
|
||||
icon: () => commentContent,
|
||||
edit: () => <Edit />,
|
||||
} as const;
|
||||
@@ -2,3 +2,4 @@ export * from './select/select';
|
||||
export * from './input/input';
|
||||
export * from './toggle/toggle';
|
||||
export * from './checkbox/checkbox';
|
||||
export * from './input/password-input';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { InputHTMLAttributes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Tooltip } from 'common/tooltip/tooltip';
|
||||
|
||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||
export type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||
customLabel?: string;
|
||||
dimension?: 'small';
|
||||
isFullWidth?: boolean;
|
||||
|
||||
35
mailpoet/assets/js/src/common/form/input/password-input.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { _x, __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
|
||||
import { Button } from 'common/button/button';
|
||||
|
||||
import { Props as InputProps, Input } from './input';
|
||||
|
||||
type Props = InputProps & {
|
||||
forceRevealed?: boolean;
|
||||
};
|
||||
|
||||
export function PasswordInput({ forceRevealed = false, ...attributes }: Props) {
|
||||
const [isRevealed, setIsRevealed] = useState(false);
|
||||
const inputType = forceRevealed || isRevealed ? 'text' : 'password';
|
||||
const toggleButton = !forceRevealed && (
|
||||
<Button
|
||||
className="mailpoet-password-input-toggle"
|
||||
variant="tertiary"
|
||||
aria-label={
|
||||
isRevealed
|
||||
? __('Hide input value', 'mailpoet')
|
||||
: __('Show input value', 'mailpoet')
|
||||
}
|
||||
onClick={() => setIsRevealed(!isRevealed)}
|
||||
>
|
||||
{isRevealed
|
||||
? // translators: Used as a button to show or hide the password
|
||||
_x('Hide', 'verb', 'mailpoet')
|
||||
: // translators: Used as a button to show or hide the password
|
||||
_x('Show', 'verb', 'mailpoet')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return <Input type={inputType} iconEnd={toggleButton} {...attributes} />;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { _x } from '@wordpress/i18n';
|
||||
import { Button, Input } from 'common/index';
|
||||
import { PasswordInput } from 'common/index';
|
||||
import { useAction, useSelector } from 'settings/store/hooks';
|
||||
import { useState } from 'react';
|
||||
|
||||
type KeyInputPropType = {
|
||||
placeholder?: string;
|
||||
@@ -16,25 +14,9 @@ export function KeyInput({
|
||||
}: KeyInputPropType) {
|
||||
const state = useSelector('getKeyActivationState')();
|
||||
const setState = useAction('updateKeyActivationState');
|
||||
const [isRevealed, setIsRevealed] = useState(false);
|
||||
const inputType = forceRevealed || isRevealed ? 'text' : 'password';
|
||||
const toggleButton = !forceRevealed && (
|
||||
<Button
|
||||
className="mailpoet-premium-key-toggle"
|
||||
variant="tertiary"
|
||||
onClick={() => setIsRevealed(!isRevealed)}
|
||||
>
|
||||
{isRevealed
|
||||
? // translators: Used as a button to show or hide the premium key
|
||||
_x('Hide', 'verb', 'mailpoet')
|
||||
: // translators: Used as a button to show or hide the premium key
|
||||
_x('Show', 'verb', 'mailpoet')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Input
|
||||
type={inputType}
|
||||
<PasswordInput
|
||||
forceRevealed={forceRevealed}
|
||||
id="mailpoet_premium_key"
|
||||
name="premium[premium_key]"
|
||||
placeholder={placeholder}
|
||||
@@ -48,7 +30,6 @@ export function KeyInput({
|
||||
key: event.target.value.trim() || null,
|
||||
})
|
||||
}
|
||||
iconEnd={toggleButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export const FeaturesController = (config) => ({
|
||||
FEATURE_BRAND_TEMPLATES: 'brand_templates',
|
||||
FEATURE_ODIE_CHATBOT: 'odie_chatbot',
|
||||
|
||||
isSupported: (feature) => {
|
||||
return config[feature] || false;
|
||||
|
||||
@@ -233,7 +233,9 @@ export function* showPreview() {
|
||||
yield changeActiveSidebar('default');
|
||||
const customFields = select(storeName).getAllAvailableCustomFields();
|
||||
const formData = select(storeName).getFormData();
|
||||
const formBlocks = select(storeName).getFormBlocks();
|
||||
// Get blocks directly from block editor store to ensure we have the latest state
|
||||
const formBlocks = select(blockEditorStore).getBlocks();
|
||||
|
||||
const blocksToFormBody = blocksToFormBodyFactory(
|
||||
FONT_SIZES,
|
||||
SETTINGS_DEFAULTS.colors,
|
||||
|
||||
@@ -33,7 +33,7 @@ export type FormSettingsType = {
|
||||
fixedBarStyles: PlacementStyles;
|
||||
fontColor?: string;
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
fontSize?: string | number;
|
||||
formPadding: number;
|
||||
formPlacement: {
|
||||
popup: FormPlacementBase & {
|
||||
@@ -105,7 +105,7 @@ export type InputBlockStyles = {
|
||||
backgroundColor?: string;
|
||||
gradient?: string;
|
||||
borderSize?: number;
|
||||
fontSize?: number;
|
||||
fontSize?: string | number;
|
||||
fontColor?: string;
|
||||
borderRadius?: number;
|
||||
borderColor?: string;
|
||||
|
||||
@@ -39,7 +39,9 @@ export const mapInputBlockStyles = (styles: InputBlockStylesServerData) => {
|
||||
mappedStyles.borderSize = Number(styles.border_size);
|
||||
}
|
||||
if (has(styles, 'font_size') && styles.font_size !== undefined) {
|
||||
mappedStyles.fontSize = Number(styles.font_size);
|
||||
mappedStyles.fontSize = !Number.isNaN(Number(styles.font_size))
|
||||
? Number(styles.font_size)
|
||||
: styles.font_size;
|
||||
}
|
||||
if (has(styles, 'font_color') && styles.font_color) {
|
||||
mappedStyles.fontColor = styles.font_color;
|
||||
|
||||
@@ -95,7 +95,7 @@ Module.DynamicProductsBlockModel = base.BlockModel.extend({
|
||||
base.BlockView.prototype.initialize.apply(this, args);
|
||||
|
||||
this.on(
|
||||
'change:amount change:contentType change:terms change:inclusionType change:displayType change:titleFormat change:featuredImagePosition change:titleAlignment change:titleIsLink change:imageFullWidth change:pricePosition change:readMoreType change:readMoreText change:sortBy change:showDivider change:dynamicProductsType change:titlePosition',
|
||||
'change:amount change:contentType change:terms change:inclusionType change:displayType change:titleFormat change:featuredImagePosition change:titleAlignment change:titleIsLink change:imageFullWidth change:pricePosition change:readMoreType change:readMoreText change:sortBy change:showDivider change:dynamicProductsType change:titlePosition change:excludeOutOfStock',
|
||||
this._handleChanges,
|
||||
this,
|
||||
);
|
||||
@@ -220,6 +220,10 @@ Module.DynamicProductsBlockSettingsView = base.BlockSettingsView.extend({
|
||||
this.changeField,
|
||||
'inclusionType',
|
||||
),
|
||||
'change .mailpoet_dynamic_products_exclude_out_of_stock': _.partial(
|
||||
this.changeBoolCheckboxField,
|
||||
'excludeOutOfStock',
|
||||
),
|
||||
'change .mailpoet_dynamic_products_title_alignment': _.partial(
|
||||
this.changeField,
|
||||
'titleAlignment',
|
||||
|
||||
@@ -315,6 +315,7 @@ SocialBlockSettingsStylesView = Marionette.View.extend({
|
||||
socialIconSets: allIconSets.toJSON(),
|
||||
availableSets: _.keys(allIconSets.toJSON()),
|
||||
availableSocialIcons: this.model.get('icons').pluck('iconType'),
|
||||
imageMissingSrc: App.getConfig().get('urls.imageMissing'),
|
||||
};
|
||||
},
|
||||
changeSocialIconSet: function (event) {
|
||||
|
||||
@@ -33,7 +33,7 @@ export function NewsletterTypes({
|
||||
}: Props): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(null);
|
||||
|
||||
const [isSelectEditorModalOpen, setIsSelectEditorModalOpen] = useState(false);
|
||||
const isNewEmailEditorEnabled = window.mailpoet_block_email_editor_enabled;
|
||||
@@ -47,49 +47,26 @@ export function NewsletterTypes({
|
||||
}
|
||||
};
|
||||
|
||||
const renderType = (type): JSX.Element => {
|
||||
const badgeClassName =
|
||||
window.mailpoet_is_new_user === true
|
||||
? 'mailpoet_badge mailpoet_badge_video'
|
||||
: 'mailpoet_badge mailpoet_badge_video mailpoet_badge_video_grey';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={type.slug}
|
||||
data-type={type.slug}
|
||||
className="mailpoet-newsletter-type"
|
||||
>
|
||||
<div className="mailpoet-newsletter-type-image" />
|
||||
<div className="mailpoet-newsletter-type-content">
|
||||
<Heading level={4}>
|
||||
{type.title} {type.beta ? `(${__('Beta', 'mailpoet')})` : ''}
|
||||
</Heading>
|
||||
<p>{type.description}</p>
|
||||
{type.videoGuide && (
|
||||
<a
|
||||
className={badgeClassName}
|
||||
href={type.videoGuide}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="dashicons dashicons-format-video" />
|
||||
{__('See video guide', 'mailpoet')}
|
||||
</a>
|
||||
)}
|
||||
{type.kbLink && (
|
||||
<a href={type.kbLink} target="_blank" rel="noopener noreferrer">
|
||||
{__('Read more.', 'mailpoet')}
|
||||
</a>
|
||||
)}
|
||||
<div className="mailpoet-flex-grow" />
|
||||
<div className="mailpoet-newsletter-type-action">{type.action}</div>
|
||||
</div>
|
||||
const renderType = (type): JSX.Element => (
|
||||
<div
|
||||
key={type.slug}
|
||||
data-type={type.slug}
|
||||
className="mailpoet-newsletter-type"
|
||||
>
|
||||
<div className="mailpoet-newsletter-type-image" />
|
||||
<div className="mailpoet-newsletter-type-content">
|
||||
<Heading level={4}>
|
||||
{type.title} {type.beta ? `(${__('Beta', 'mailpoet')})` : ''}
|
||||
</Heading>
|
||||
<p>{type.description}</p>
|
||||
<div className="mailpoet-flex-grow" />
|
||||
<div className="mailpoet-newsletter-type-action">{type.action}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
const createNewsletter = (type): void => {
|
||||
setIsCreating(true);
|
||||
setIsCreating(type);
|
||||
MailPoet.trackEvent('Emails > Type selected', {
|
||||
'Email type': type,
|
||||
});
|
||||
@@ -106,7 +83,7 @@ export function NewsletterTypes({
|
||||
navigate(`/template/${response.data.id}`);
|
||||
})
|
||||
.fail((response) => {
|
||||
setIsCreating(false);
|
||||
setIsCreating(null);
|
||||
if (response.errors.length > 0) {
|
||||
return <APIErrorsNotice errors={response.errors} />;
|
||||
}
|
||||
@@ -124,7 +101,7 @@ export function NewsletterTypes({
|
||||
're-engagement',
|
||||
);
|
||||
const createAutomation = () => {
|
||||
setIsCreating(true);
|
||||
setIsCreating('automation');
|
||||
window.location.href = 'admin.php?page=mailpoet-automation-templates';
|
||||
};
|
||||
|
||||
@@ -133,7 +110,8 @@ export function NewsletterTypes({
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={createStandardNewsletter}
|
||||
isBusy={isCreating}
|
||||
isBusy={isCreating === 'standard'}
|
||||
disabled={isCreating !== null}
|
||||
data-automation-id="create_standard"
|
||||
>
|
||||
{__('Create', 'mailpoet')}
|
||||
@@ -148,6 +126,8 @@ export function NewsletterTypes({
|
||||
variant="secondary"
|
||||
className="mailpoet-button-with-wordpress-icon"
|
||||
onClick={onToggle}
|
||||
isBusy={isCreating === 'standard'}
|
||||
disabled={isCreating !== null}
|
||||
aria-expanded={isOpen}
|
||||
data-automation-id="create_standard_email_dropdown"
|
||||
>
|
||||
@@ -214,7 +194,8 @@ export function NewsletterTypes({
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={createAutomation}
|
||||
isBusy={isCreating}
|
||||
isBusy={isCreating === 'automation'}
|
||||
disabled={isCreating !== null}
|
||||
data-automation-id="create_automation"
|
||||
>
|
||||
{__('Create', 'mailpoet')}
|
||||
@@ -234,7 +215,8 @@ export function NewsletterTypes({
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={createNotificationNewsletter}
|
||||
isBusy={isCreating}
|
||||
isBusy={isCreating === 'notification'}
|
||||
disabled={isCreating !== null}
|
||||
data-automation-id="create_notification"
|
||||
>
|
||||
{__('Create', 'mailpoet')}
|
||||
@@ -252,7 +234,8 @@ export function NewsletterTypes({
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={createReEngagementNewsletter}
|
||||
isBusy={isCreating}
|
||||
isBusy={isCreating === 're_engagement'}
|
||||
disabled={isCreating !== null}
|
||||
data-automation-id="create_notification"
|
||||
>
|
||||
{__('Create', 'mailpoet')}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Scheduling } from './scheduling';
|
||||
import { ListingHeadingStepsRoute } from '../../listings/heading-steps-route';
|
||||
|
||||
export function NewsletterTypeReEngagement(): JSX.Element {
|
||||
let defaultAfterTime = '';
|
||||
let defaultAfterTime = '11';
|
||||
if (MailPoet.deactivateSubscriberAfterInactiveDays) {
|
||||
defaultAfterTime = (
|
||||
Math.floor(Number(MailPoet.deactivateSubscriberAfterInactiveDays) / 30) -
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Label, Inputs } from 'settings/components';
|
||||
import { t, onChange } from 'common/functions';
|
||||
import { Input } from 'common/form/input/input';
|
||||
import { PasswordInput } from 'common/form';
|
||||
import { Select } from 'common/form/select/select';
|
||||
import { useSetting, useSelector } from 'settings/store/hooks';
|
||||
import { SendingFrequency } from './sending-frequency';
|
||||
@@ -35,9 +35,8 @@ export function AmazonSesFields() {
|
||||
</Inputs>
|
||||
<Label title={t('accessKey')} htmlFor="mailpoet_amazon_ses_access_key" />
|
||||
<Inputs>
|
||||
<Input
|
||||
<PasswordInput
|
||||
dimension="small"
|
||||
type="text"
|
||||
value={accessKey}
|
||||
className="regular-text"
|
||||
onChange={onChange(setAccessKey)}
|
||||
@@ -46,9 +45,8 @@ export function AmazonSesFields() {
|
||||
</Inputs>
|
||||
<Label title={t('secretKey')} htmlFor="mailpoet_amazon_ses_secret_key" />
|
||||
<Inputs>
|
||||
<Input
|
||||
<PasswordInput
|
||||
dimension="small"
|
||||
type="text"
|
||||
value={secretKey}
|
||||
className="regular-text"
|
||||
onChange={onChange(setSecretKey)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Label, Inputs } from 'settings/components';
|
||||
import { t, onChange } from 'common/functions';
|
||||
import { Input } from 'common/form/input/input';
|
||||
import { PasswordInput } from 'common/form';
|
||||
import { useSetting, useSelector } from 'settings/store/hooks';
|
||||
import { SendingFrequency } from './sending-frequency';
|
||||
|
||||
@@ -15,9 +15,8 @@ export function SendGridFields() {
|
||||
/>
|
||||
<Label title={t('apiKey')} htmlFor="mailpoet_sendgrid_api_key" />
|
||||
<Inputs>
|
||||
<Input
|
||||
<PasswordInput
|
||||
dimension="small"
|
||||
type="text"
|
||||
value={apiKey}
|
||||
onChange={onChange(setApiKey)}
|
||||
id="mailpoet_sendgrid_api_key"
|
||||
|
||||
@@ -1,19 +1,47 @@
|
||||
import jQuery from 'jquery';
|
||||
import { MailPoet } from 'mailpoet';
|
||||
|
||||
export const createNewSegment = (onCreateSegment) => {
|
||||
interface Segment {
|
||||
id: string;
|
||||
name: string;
|
||||
text: string;
|
||||
subscriberCount: number;
|
||||
}
|
||||
|
||||
interface CreateSegmentResponse {
|
||||
data: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ApiErrorResponse {
|
||||
errors: Array<{
|
||||
error: string;
|
||||
message: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const createNewSegment = (
|
||||
onCreateSegment: (segment: Segment) => void,
|
||||
): void => {
|
||||
MailPoet.Modal.popup({
|
||||
title: MailPoet.I18n.t('addNewList'),
|
||||
template: jQuery('#new_segment_template').html(),
|
||||
});
|
||||
jQuery('#new_segment_name').on('keypress', (e) => {
|
||||
|
||||
jQuery('#new_segment_name').on('keypress', (e: JQuery.KeyPressEvent) => {
|
||||
if (e.which === 13) {
|
||||
jQuery('#new_segment_process').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
jQuery('#new_segment_process').on('click', () => {
|
||||
const segmentName = jQuery('#new_segment_name').val().trim();
|
||||
const segmentDescription = jQuery('#new_segment_description').val().trim();
|
||||
const segmentName: string =
|
||||
jQuery('#new_segment_name').val()?.toString().trim() || '';
|
||||
const segmentDescription: string =
|
||||
jQuery('#new_segment_description').val()?.toString().trim() || '';
|
||||
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
@@ -24,7 +52,7 @@ export const createNewSegment = (onCreateSegment) => {
|
||||
description: segmentDescription,
|
||||
},
|
||||
})
|
||||
.done((response) => {
|
||||
.done((response: CreateSegmentResponse) => {
|
||||
onCreateSegment({
|
||||
id: response.data.id,
|
||||
name: response.data.name,
|
||||
@@ -34,15 +62,17 @@ export const createNewSegment = (onCreateSegment) => {
|
||||
|
||||
MailPoet.Modal.close();
|
||||
})
|
||||
.fail((response) => {
|
||||
.fail((response: ApiErrorResponse) => {
|
||||
if (response.errors.length > 0) {
|
||||
MailPoet.Notice.hide();
|
||||
MailPoet.Notice.showApiErrorNotice(response, {
|
||||
positionAfter: '#new_segment_name',
|
||||
positionAfter: '#new_segment_error_message',
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
jQuery('#new_segment_cancel').on('click', () => {
|
||||
MailPoet.Modal.close();
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
createSelection,
|
||||
destroySelection,
|
||||
} from './generate-segment-selection.jsx';
|
||||
import { createNewSegment } from './create-new-segment.jsx';
|
||||
import { createNewSegment } from './create-new-segment';
|
||||
|
||||
function SelectSegment({ setSelectedSegments }) {
|
||||
const { segments: segmentsContext } = useContext(GlobalContext);
|
||||
|
||||
@@ -176,232 +176,245 @@ const createModal = (submitModal, closeModal, field, title) => (
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const getBulkActions = () => [
|
||||
{
|
||||
name: 'moveToList',
|
||||
label: __('Move to list...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'move_to_segment',
|
||||
name: 'move_to_segment',
|
||||
endpoint: 'segments',
|
||||
filter: function filter(segment) {
|
||||
return !!(!segment.deleted_at && segment.type === 'default');
|
||||
},
|
||||
};
|
||||
const getBulkActions = () => {
|
||||
const bulkActions = [
|
||||
{
|
||||
name: 'moveToList',
|
||||
label: __('Move to list...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'move_to_segment',
|
||||
name: 'move_to_segment',
|
||||
endpoint: 'segments',
|
||||
filter: function filter(segment) {
|
||||
return !!(!segment.deleted_at && segment.type === 'default');
|
||||
},
|
||||
};
|
||||
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Move to list...', 'mailpoet'),
|
||||
);
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Move to list...', 'mailpoet'),
|
||||
);
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
segment_id: Number(jQuery('#move_to_segment').val()),
|
||||
};
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'%1$d subscribers were moved to list <strong>%2$s</strong>.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$d', Number(response.meta.count).toLocaleString())
|
||||
.replace('%2$s', response.meta.segment),
|
||||
);
|
||||
},
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
segment_id: Number(jQuery('#move_to_segment').val()),
|
||||
};
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'%1$d subscribers were moved to list <strong>%2$s</strong>.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$d', Number(response.meta.count).toLocaleString())
|
||||
.replace('%2$s', response.meta.segment),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'addToList',
|
||||
label: __('Add to list...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'add_to_segment',
|
||||
name: 'add_to_segment',
|
||||
endpoint: 'segments',
|
||||
filter: function filter(segment) {
|
||||
return !!(!segment.deleted_at && segment.type === 'default');
|
||||
},
|
||||
};
|
||||
{
|
||||
name: 'addToList',
|
||||
label: __('Add to list...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'add_to_segment',
|
||||
name: 'add_to_segment',
|
||||
endpoint: 'segments',
|
||||
filter: function filter(segment) {
|
||||
return !!(!segment.deleted_at && segment.type === 'default');
|
||||
},
|
||||
};
|
||||
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Add to list...', 'mailpoet'),
|
||||
);
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Add to list...', 'mailpoet'),
|
||||
);
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
segment_id: Number(jQuery('#add_to_segment').val()),
|
||||
};
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'%1$d subscribers were added to list <strong>%2$s</strong>.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$d', Number(response.meta.count).toLocaleString())
|
||||
.replace('%2$s', response.meta.segment),
|
||||
);
|
||||
},
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
segment_id: Number(jQuery('#add_to_segment').val()),
|
||||
};
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'%1$d subscribers were added to list <strong>%2$s</strong>.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$d', Number(response.meta.count).toLocaleString())
|
||||
.replace('%2$s', response.meta.segment),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'removeFromList',
|
||||
label: __('Remove from list...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'remove_from_segment',
|
||||
name: 'remove_from_segment',
|
||||
endpoint: 'segments',
|
||||
filter: function filter(segment) {
|
||||
return segment.type === 'default';
|
||||
},
|
||||
};
|
||||
{
|
||||
name: 'removeFromList',
|
||||
label: __('Remove from list...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'remove_from_segment',
|
||||
name: 'remove_from_segment',
|
||||
endpoint: 'segments',
|
||||
filter: function filter(segment) {
|
||||
return segment.type === 'default';
|
||||
},
|
||||
};
|
||||
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Remove from list...', 'mailpoet'),
|
||||
);
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Remove from list...', 'mailpoet'),
|
||||
);
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
segment_id: Number(jQuery('#remove_from_segment').val()),
|
||||
};
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'%1$d subscribers were removed from list <strong>%2$s</strong>.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$d', Number(response.meta.count).toLocaleString())
|
||||
.replace('%2$s', response.meta.segment),
|
||||
);
|
||||
},
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
segment_id: Number(jQuery('#remove_from_segment').val()),
|
||||
};
|
||||
{
|
||||
name: 'removeFromAllLists',
|
||||
label: __('Remove from all lists', 'mailpoet'),
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'%1$d subscribers were removed from all lists.',
|
||||
'mailpoet',
|
||||
).replace('%1$d', Number(response.meta.count).toLocaleString()),
|
||||
);
|
||||
},
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'%1$d subscribers were removed from list <strong>%2$s</strong>.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$d', Number(response.meta.count).toLocaleString())
|
||||
.replace('%2$s', response.meta.segment),
|
||||
);
|
||||
{
|
||||
name: 'trash',
|
||||
label: __('Move to trash', 'mailpoet'),
|
||||
onSuccess: getMessages().onTrash,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'removeFromAllLists',
|
||||
label: __('Remove from all lists', 'mailpoet'),
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__('%1$d subscribers were removed from all lists.', 'mailpoet').replace(
|
||||
'%1$d',
|
||||
Number(response.meta.count).toLocaleString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'trash',
|
||||
label: __('Move to trash', 'mailpoet'),
|
||||
onSuccess: getMessages().onTrash,
|
||||
},
|
||||
{
|
||||
name: 'unsubscribe',
|
||||
label: __('Unsubscribe', 'mailpoet'),
|
||||
onSelect: (submitModal, closeModal, bulkActionProps) => {
|
||||
const count =
|
||||
bulkActionProps.selection !== 'all'
|
||||
? bulkActionProps.selected_ids.length
|
||||
: bulkActionProps.count;
|
||||
return (
|
||||
<Modal
|
||||
title={__('Unsubscribe', 'mailpoet')}
|
||||
onRequestClose={closeModal}
|
||||
isDismissible
|
||||
>
|
||||
<p>
|
||||
{__(
|
||||
'This action will unsubscribe %s subscribers from all lists. This action cannot be undone. Are you sure, you want to continue?',
|
||||
'mailpoet',
|
||||
).replace('%s', Number(count).toLocaleString())}
|
||||
</p>
|
||||
<span className="mailpoet-gap-half" />
|
||||
<Button
|
||||
onClick={submitModal}
|
||||
dimension="small"
|
||||
variant="secondary"
|
||||
automationId="bulk-unsubscribe-confirm"
|
||||
{
|
||||
name: 'unsubscribe',
|
||||
label: __('Unsubscribe', 'mailpoet'),
|
||||
onSelect: (submitModal, closeModal, bulkActionProps) => {
|
||||
const count =
|
||||
bulkActionProps.selection !== 'all'
|
||||
? bulkActionProps.selected_ids.length
|
||||
: bulkActionProps.count;
|
||||
return (
|
||||
<Modal
|
||||
title={__('Unsubscribe', 'mailpoet')}
|
||||
onRequestClose={closeModal}
|
||||
isDismissible
|
||||
>
|
||||
{__('Apply', 'mailpoet')}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
<p>
|
||||
{__(
|
||||
'This action will unsubscribe %s subscribers from all lists. This action cannot be undone. Are you sure, you want to continue?',
|
||||
'mailpoet',
|
||||
).replace('%s', Number(count).toLocaleString())}
|
||||
</p>
|
||||
<span className="mailpoet-gap-half" />
|
||||
<Button
|
||||
onClick={submitModal}
|
||||
dimension="small"
|
||||
variant="secondary"
|
||||
automationId="bulk-unsubscribe-confirm"
|
||||
>
|
||||
{__('Apply', 'mailpoet')}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'addTag',
|
||||
label: __('Add tag...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'add_tag',
|
||||
name: 'add_tag',
|
||||
endpoint: 'tags',
|
||||
};
|
||||
{
|
||||
name: 'addTag',
|
||||
label: __('Add tag...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'add_tag',
|
||||
name: 'add_tag',
|
||||
endpoint: 'tags',
|
||||
};
|
||||
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Add tag...', 'mailpoet'),
|
||||
);
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Add tag...', 'mailpoet'),
|
||||
);
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
tag_id: Number(jQuery('#add_tag').val()),
|
||||
};
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'Tag <strong>%1$s</strong> was added to %2$d subscribers.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$s', response.meta.tag)
|
||||
.replace('%2$d', Number(response.meta.count).toLocaleString()),
|
||||
);
|
||||
},
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
tag_id: Number(jQuery('#add_tag').val()),
|
||||
};
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'Tag <strong>%1$s</strong> was added to %2$d subscribers.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$s', response.meta.tag)
|
||||
.replace('%2$d', Number(response.meta.count).toLocaleString()),
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'removeTag',
|
||||
label: __('Remove tag...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'remove_tag',
|
||||
name: 'remove_tag',
|
||||
endpoint: 'tags',
|
||||
};
|
||||
{
|
||||
name: 'removeTag',
|
||||
label: __('Remove tag...', 'mailpoet'),
|
||||
onSelect: function onSelect(submitModal, closeModal) {
|
||||
const field = {
|
||||
id: 'remove_tag',
|
||||
name: 'remove_tag',
|
||||
endpoint: 'tags',
|
||||
};
|
||||
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Remove tag...', 'mailpoet'),
|
||||
);
|
||||
return createModal(
|
||||
submitModal,
|
||||
closeModal,
|
||||
field,
|
||||
__('Remove tag...', 'mailpoet'),
|
||||
);
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
tag_id: Number(jQuery('#remove_tag').val()),
|
||||
};
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'Tag <strong>%1$s</strong> was removed from %2$d subscribers.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$s', response.meta.tag)
|
||||
.replace('%2$d', Number(response.meta.count).toLocaleString()),
|
||||
);
|
||||
},
|
||||
},
|
||||
getData: function getData() {
|
||||
return {
|
||||
tag_id: Number(jQuery('#remove_tag').val()),
|
||||
};
|
||||
},
|
||||
onSuccess: function onSuccess(response: Response) {
|
||||
MailPoet.Notice.success(
|
||||
__(
|
||||
'Tag <strong>%1$s</strong> was removed from %2$d subscribers.',
|
||||
'mailpoet',
|
||||
)
|
||||
.replace('%1$s', response.meta.tag)
|
||||
.replace('%2$d', Number(response.meta.count).toLocaleString()),
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
// Filter out 'unsubscribe' action if we're in the unsubscribed group
|
||||
const url = window.location.href;
|
||||
const match = url.match(/group\[(.*?)\]/);
|
||||
const group = match ? match[1] : null;
|
||||
|
||||
if (group === 'unsubscribed') {
|
||||
return bulkActions.filter((action) => action.name !== 'unsubscribe');
|
||||
}
|
||||
|
||||
return bulkActions;
|
||||
};
|
||||
|
||||
const getItemActions = () => [
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@ function WelcomeWizardUsageTrackingStep({ loading, submitForm }) {
|
||||
<div className="mailpoet-wizard-note">
|
||||
{ReactStringReplace(
|
||||
__(
|
||||
'MailPoet may load Google Fonts, DocsBot and other [link]3rd party libraries[/link].',
|
||||
'MailPoet may load Google Fonts, WordPress.com and other [link]3rd party libraries[/link].',
|
||||
'mailpoet',
|
||||
),
|
||||
/\[link\](.*?)\[\/link\]/g,
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
== Changelog ==
|
||||
|
||||
= 5.13.2 - 2025-08-20 =
|
||||
* Fixed: revert 5.13.1 release.
|
||||
|
||||
= 5.13.0 - 2025-08-12 =
|
||||
* Added: Add duplication of an automation step;
|
||||
* Added: "Active WooCommerce subscriptions count" field in automation filters;
|
||||
* Updated: Bump the minimum required WooCommerce version to 10.0 and tested up to version to 10.1;
|
||||
* Improved: Ensure that logging of WooCommerce First Purchase is done only when necessary.
|
||||
|
||||
= 5.12.13 - 2025-08-04 =
|
||||
* Added: The dynamic products block can filter out out-of-stock products;
|
||||
* Fixed: Prevent automatic "Subscribed" status for guest order subscribers when Sign-up Confirmation is disabled.
|
||||
|
||||
= 5.12.12 - 2025-07-22 =
|
||||
* Added: A new Automation Trigger when a new WooCommerce order note is added;
|
||||
* Updated: minimum required WooCommerce version to 9.9 and tested up to version to 10.0.
|
||||
|
||||
= 5.12.11 - 2025-07-14 =
|
||||
* Improved: JavaScript and CSS compatibility with other plugins and themes;
|
||||
* Fixed: inconsistent spacing when adding tags to subscribers;
|
||||
* Fixed: error message position when creating a list during import;
|
||||
* Fixed: page title replacement broken on non-English sites.
|
||||
|
||||
= 5.12.10 - 2025-07-07 =
|
||||
* Improved: SendGrid API key field and Amazon SES access key and secret key fields now use masked input fields to prevent accidental credential exposure;
|
||||
* Fixed: PHP Warning: Undefined array key “blocks”;
|
||||
* Fixed: filter `mailpoet_unsubscribe_confirmation_page` not redirecting to the success page.
|
||||
|
||||
= 5.12.9 - 2025-06-30 =
|
||||
* Improved: handling of MailPoet Page title when switching website languages;
|
||||
* Improved: add default re-engagement emails trigger value when "Inactive subscribers" feature is turned off;
|
||||
* Improved: when choosing an email type, show loading animation only on the selected one;
|
||||
* Changed: replaced DocsBot with WordPress.com chatbot when searching MailPoet Knowledge Base;
|
||||
* Fixed: rendering submit button when font size is changed;
|
||||
* Fixed: fatal error in ACF and SCF when working with post templates in location rules;
|
||||
* Fixed: apply the latest changes in the form editor in the preview.
|
||||
|
||||
= 5.12.8 - 2025-06-24 =
|
||||
* Updated: support custom order statuses in WooCommerce segments;
|
||||
* Improved: adds `polylang` to the list of permitted scripts;
|
||||
* Fixed: warning `Undefined array key "blocks"`.
|
||||
|
||||
= 5.12.7 - 2025-06-16 =
|
||||
* Updated: minimum required WooCommerce version to 9.8 and tested up to version to 9.9.
|
||||
|
||||
= 5.12.6 - 2025-06-09 =
|
||||
* Added: new networks in social icons block (Behance, Bluesky, Discord, GitHub, Gravatar, Mastodon, Medium, Patreon, Reddit, RSS, Spotify, Telegram, Threads, TikTok, Tumblr, Twitch, Viemo, WhatsApp, WordPress);
|
||||
* Improved: updated existing social networks with the official icons;
|
||||
* Improved: hide 'unsubscribe' bulk action when segmenting unsubscribed subscribers.
|
||||
|
||||
= 5.12.5 - 2025-06-02 =
|
||||
* Improved: Add "4th" day of week as a monthly frequency option;
|
||||
* Fixed: issue where duplicated newsletters could be incorrectly linked to unrelated posts under certain conditions.
|
||||
|
||||
1
mailpoet/changelog/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -78,14 +78,16 @@
|
||||
"./tools/vendor/composer.phar --working-dir=tasks/phpstan install",
|
||||
"./tools/vendor/composer.phar --working-dir=../tests_env install",
|
||||
"php ./tasks/fix-guzzle.php",
|
||||
"php ./tasks/fix-php82-deprecations.php"
|
||||
"php ./tasks/fix-php82-deprecations.php",
|
||||
"php ./tasks/FixPhp84Deprecations.php"
|
||||
],
|
||||
"post-install-cmd": [
|
||||
"./tools/vendor/composer.phar --working-dir=tasks/code_sniffer install",
|
||||
"./tools/vendor/composer.phar --working-dir=tasks/phpstan install",
|
||||
"./tools/vendor/composer.phar --working-dir=../tests_env install",
|
||||
"php ./tasks/fix-guzzle.php",
|
||||
"php ./tasks/fix-php82-deprecations.php"
|
||||
"php ./tasks/fix-php82-deprecations.php",
|
||||
"php ./tasks/FixPhp84Deprecations.php"
|
||||
],
|
||||
"pre-autoload-dump": [
|
||||
"php ./tasks/fix-codeception-stub.php",
|
||||
|
||||
@@ -204,7 +204,7 @@ class PageRenderer {
|
||||
'name' => $tag->getName(),
|
||||
];
|
||||
}, $this->tagRepository->findAll()),
|
||||
'display_docsbot_widget' => $this->displayDocsBotWidget(),
|
||||
'display_chatbot_widget' => $this->displayChatBotWidget(),
|
||||
'is_woocommerce_subscriptions_active' => $this->wooCommerceSubscriptionsHelper->isWooCommerceSubscriptionsActive(),
|
||||
'cron_trigger_method' => $this->settings->get('cron_trigger.method'),
|
||||
];
|
||||
@@ -252,7 +252,7 @@ class PageRenderer {
|
||||
];
|
||||
}
|
||||
|
||||
public function displayDocsBotWidget(): bool {
|
||||
public function displayChatBotWidget(): bool {
|
||||
$display = $this->wp->applyFilters('mailpoet_display_docsbot_widget', $this->settings->get('3rd_party_libs.enabled') === '1');
|
||||
return (bool)$display;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ use MailPoet\Captcha\CaptchaConstants;
|
||||
use MailPoet\Config\ServicesChecker;
|
||||
use MailPoet\Cron\CronTrigger;
|
||||
use MailPoet\Entities\DynamicSegmentFilterData;
|
||||
use MailPoet\Entities\FormEntity;
|
||||
use MailPoet\Form\FormsRepository;
|
||||
use MailPoet\Listing\ListingDefinition;
|
||||
use MailPoet\Newsletter\NewslettersRepository;
|
||||
use MailPoet\Segments\DynamicSegments\DynamicSegmentFilterRepository;
|
||||
@@ -106,6 +108,9 @@ class Reporter {
|
||||
/*** @var ReporterCampaignData */
|
||||
private $reporterCampaignData;
|
||||
|
||||
/** @var FormsRepository */
|
||||
private $formsRepository;
|
||||
|
||||
public function __construct(
|
||||
NewslettersRepository $newslettersRepository,
|
||||
SegmentsRepository $segmentsRepository,
|
||||
@@ -121,7 +126,8 @@ class Reporter {
|
||||
AutomationStorage $automationStorage,
|
||||
UnsubscribeReporter $unsubscribeReporter,
|
||||
DotcomHelperFunctions $dotcomHelperFunctions,
|
||||
ReporterCampaignData $reporterCampaignData
|
||||
ReporterCampaignData $reporterCampaignData,
|
||||
FormsRepository $formsRepository
|
||||
) {
|
||||
$this->newslettersRepository = $newslettersRepository;
|
||||
$this->segmentsRepository = $segmentsRepository;
|
||||
@@ -138,6 +144,7 @@ class Reporter {
|
||||
$this->unsubscribeReporter = $unsubscribeReporter;
|
||||
$this->dotcomHelperFunctions = $dotcomHelperFunctions;
|
||||
$this->reporterCampaignData = $reporterCampaignData;
|
||||
$this->formsRepository = $formsRepository;
|
||||
}
|
||||
|
||||
public function getData() {
|
||||
@@ -150,6 +157,7 @@ class Reporter {
|
||||
$hasWc = $this->woocommerceHelper->isWooCommerceActive();
|
||||
$inactiveSubscribersMonths = (int)round((int)$this->settings->get('deactivate_subscriber_after_inactive_days') / 30);
|
||||
$inactiveSubscribersStatus = $inactiveSubscribersMonths === 0 ? 'never' : "$inactiveSubscribersMonths months";
|
||||
$activeFormCounts = $this->formsRepository->getActiveFormsCountByType();
|
||||
|
||||
$result = [
|
||||
'PHP version' => PHP_VERSION,
|
||||
@@ -269,6 +277,18 @@ class Reporter {
|
||||
'Sign-up confirmation: Confirmation Template > using html email editor template' => (boolean)$this->settings->get(ConfirmationEmailCustomizer::SETTING_ENABLE_EMAIL_CUSTOMIZER, false),
|
||||
'Is WordPress.com' => $this->dotcomHelperFunctions->isDotcom() ? 'yes' : 'no',
|
||||
'WordPress.com plan' => $this->dotcomHelperFunctions->getDotcomPlan(),
|
||||
'Forms > Number of active forms' => $activeFormCounts['all'],
|
||||
'Forms > Number of active Below pages forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_BELOW_POST],
|
||||
'Forms > Number of active Fixed bar forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_FIXED_BAR],
|
||||
'Forms > Number of active Pop-up forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_POPUP],
|
||||
'Forms > Number of active Slide–in forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_SLIDE_IN],
|
||||
'Forms > Number of active Others (widget) forms' => $activeFormCounts[FormEntity::DISPLAY_TYPE_OTHERS],
|
||||
'Forms > Number of active forms with first name' => $activeFormCounts['with_first_name'],
|
||||
'Forms > Number of active forms with last name' => $activeFormCounts['with_last_name'],
|
||||
'Forms > Number of active forms with custom fields' => $activeFormCounts['with_custom_fields'],
|
||||
'Forms > Min custom fields' => $activeFormCounts['min_custom_fields'],
|
||||
'Forms > Max custom fields' => $activeFormCounts['max_custom_fields'],
|
||||
'Forms > Average custom fields' => $activeFormCounts['average_custom_fields'],
|
||||
];
|
||||
|
||||
$result = array_merge(
|
||||
|
||||
@@ -125,17 +125,18 @@ class FirstPurchase {
|
||||
$meta = $queue->getMeta();
|
||||
$result = (!empty($meta['order_date'])) ? WPFunctions::get()->dateI18n(get_option('date_format'), $meta['order_date']) : $defaultValue;
|
||||
}
|
||||
|
||||
$this->loggerFactory->getLogger(self::SLUG)->info(
|
||||
'handleOrderDateShortcode called',
|
||||
[
|
||||
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
|
||||
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
|
||||
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
|
||||
'shortcode' => $shortcode,
|
||||
'result' => $result,
|
||||
]
|
||||
);
|
||||
}
|
||||
$this->loggerFactory->getLogger(self::SLUG)->info(
|
||||
'handleOrderDateShortcode called',
|
||||
[
|
||||
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
|
||||
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
|
||||
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
|
||||
'shortcode' => $shortcode,
|
||||
'result' => $result,
|
||||
]
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -149,17 +150,18 @@ class FirstPurchase {
|
||||
$meta = $queue->getMeta();
|
||||
$result = (!empty($meta['order_amount'])) ? $this->helper->wcPrice($meta['order_amount']) : $defaultValue;
|
||||
}
|
||||
|
||||
$this->loggerFactory->getLogger(self::SLUG)->info(
|
||||
'handleOrderTotalShortcode called',
|
||||
[
|
||||
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
|
||||
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
|
||||
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
|
||||
'shortcode' => $shortcode,
|
||||
'result' => $result,
|
||||
]
|
||||
);
|
||||
}
|
||||
$this->loggerFactory->getLogger(self::SLUG)->info(
|
||||
'handleOrderTotalShortcode called',
|
||||
[
|
||||
'newsletter_id' => ($newsletter instanceof NewsletterEntity) ? $newsletter->getId() : null,
|
||||
'subscriber_id' => ($subscriber instanceof SubscriberEntity) ? $subscriber->getId() : null,
|
||||
'task_id' => ($queue instanceof SendingQueueEntity) ? (($task = $queue->getTask()) ? $task->getId() : null) : null,
|
||||
'shortcode' => $shortcode,
|
||||
'result' => $result,
|
||||
]
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||