Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
0e5281c
dev: refactor the setting field modal
Apr 8, 2026
9f9b6ed
dev: use typescript
Apr 14, 2026
116ee74
dev: use tanstack form
Apr 14, 2026
e436bfc
dev: pro features UI
Apr 15, 2026
cfe1731
dev: more UI components import
Apr 16, 2026
3684730
dev: cropper & quantities
Apr 16, 2026
f9f9c85
dev: progress
Apr 16, 2026
e09df71
dev: migrate to Chackra v3
Apr 16, 2026
ffb72c7
dev: color pallet
Apr 16, 2026
3e9252f
dev: UI tweaks
Apr 17, 2026
1036cc6
dev: tweaks
Apr 17, 2026
b215511
dev: refactoring
Apr 17, 2026
c51ece2
dev: save progress
Apr 20, 2026
c3acd5c
dev: save
Apr 20, 2026
05cc34a
dev: save progress
Apr 20, 2026
f64894c
dev: save progress
Apr 20, 2026
7292494
dev: save progress
Apr 20, 2026
190b12f
dev: add sidebar preview
Apr 21, 2026
5a6ef50
dev: split in small components
Apr 21, 2026
409d54f
dev: save progress
Apr 22, 2026
3084870
dev: do not make error persistent in the modal
Apr 22, 2026
3db89d8
dev: model footer & header
Apr 22, 2026
12410ba
dev: confirm twice to close the modal after adding some data.
Apr 22, 2026
b1f7036
dev: format & save progress
Apr 23, 2026
dd3f76e
dev: tweaks images field
Apr 23, 2026
9b47749
dev: use drag & drop API
Apr 24, 2026
6f234a1
dev: upsell more compact
Apr 24, 2026
e8a1848
dev: pick field UI design
Apr 24, 2026
499d598
dev: fix confirmation for cancel
Apr 24, 2026
eed4409
dev: ui tweak
Apr 24, 2026
c87b96c
dev: normalize the select option design
Apr 24, 2026
134c62b
dev: switch to modal react only
Apr 24, 2026
c4838cd
dev: missing settings
Apr 24, 2026
0bebd8d
Merge branch 'development' into dev/modal-refactoring
Apr 24, 2026
fbce890
dev: i18n
Apr 24, 2026
3caccf4
dev: translation loading
Apr 24, 2026
04e3f2a
dev: phpcs
Apr 24, 2026
e9fe11d
dev: labels
Apr 24, 2026
1fba45c
dev: use 3 columns format
Apr 28, 2026
fe88cc7
Merge branch 'development' into dev/modal-refactoring
Apr 28, 2026
64b8e03
chore: try fix e2e
Apr 30, 2026
0335892
Merge branch 'development' into dev/modal-refactoring
Apr 30, 2026
1d6d71a
chore: fix e2e
Apr 30, 2026
70d4c5e
chore: ui teaks for Advanced Setting toggle
Apr 30, 2026
ba2def4
chore: format js
Apr 30, 2026
c0b1b12
chore: tabs separator
Apr 30, 2026
57884e6
chore: ui tweaks for repeater and conditions
Apr 30, 2026
5f923b1
chore: UI tweaks
Apr 30, 2026
960e5e0
chore: review labels description
Soare-Robert-Daniel May 4, 2026
cb55eb4
fix: conditional repeater design
Soare-Robert-Daniel May 4, 2026
fa353dc
Merge branch 'development' into dev/modal-refactoring
Soare-Robert-Daniel May 4, 2026
e649a10
chore: upsell for Conditions
Soare-Robert-Daniel May 4, 2026
e08c4a7
chore: grouping and checkbox
Soare-Robert-Daniel May 4, 2026
d1790ec
chore: link with the copy button
Soare-Robert-Daniel May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
npm ci
npm install -g playwright-cli
npx playwright install --with-deps chromium
- name: Build admin field modal bundle
run: npm run build:admin-field-modal
- name: Install composer deps
run: composer install --no-dev
- name: Install environment
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ languages/woocommerce-product-addon.pot
artifacts
.phpunit.result.cache
tests/e2e/fixtures/generated/
packages/admin/field-modal/build/
126 changes: 116 additions & 10 deletions bin/wp-env/mu-plugins/ppom-e2e-bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,108 @@
define( 'PPOM_E2E_LICENSE_FILTER_PRIORITY', PHP_INT_MAX - 10 );
}

/**
* Ensure product fixture pages render through WooCommerce's product template.
*
* Some core test themes can render products through the generic single template,
* which skips WooCommerce add-to-cart hooks and therefore skips PPOM fields.
*
* @param string $template Current template path.
* @return string
*/
function ppom_e2e_force_single_product_template( $template ) {
$post_type = get_post_type();
if ( 'product' === $post_type && function_exists( 'WC' ) ) {
$product_template = WC()->plugin_path() . '/templates/single-product.php';
if ( file_exists( $product_template ) ) {
return $product_template;
}
}

return $template;
}
add_filter( 'template_include', 'ppom_e2e_force_single_product_template', 99 );

/**
* Register an E2E-only product render query var.
*
* @param array<int,string> $vars Public query vars.
* @return array<int,string>
*/
function ppom_e2e_product_page_query_vars( $vars ) {
$vars[] = 'ppom_e2e_product_page';
return $vars;
}
add_filter( 'query_vars', 'ppom_e2e_product_page_query_vars' );

/**
* Render product fixtures through WooCommerce's product_page shortcode.
*
* @return void
*/
function ppom_e2e_render_product_page_route() {
$product_id = absint( get_query_var( 'ppom_e2e_product_page' ) );
if ( ! $product_id ) {
return;
}

$product_post = get_post( $product_id );
if ( ! $product_post || 'product' !== $product_post->post_type ) {
status_header( 404 );
exit;
}

status_header( 200 );
get_header();
global $post, $product;
$post = $product_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- E2E render route.
$product = wc_get_product( $product_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- E2E render route.
setup_postdata( $post );
echo '<main id="primary" class="site-main single-product">';
echo '<div class="product">';
echo '<h1 class="product_title entry-title">' . esc_html( get_the_title( $product_id ) ) . '</h1>';
echo '<form class="cart" method="post">';
if ( $product && $product->is_type( 'variable' ) && method_exists( $product, 'get_variation_attributes' ) ) {
foreach ( $product->get_variation_attributes() as $attribute_name => $options ) {
$select_name = 'attribute_' . sanitize_title( $attribute_name );
echo '<select name="' . esc_attr( $select_name ) . '">';
echo '<option value="">' . esc_html__( 'Choose an option', 'woocommerce-product-addon' ) . '</option>';
foreach ( $options as $option ) {
echo '<option value="' . esc_attr( (string) $option ) . '">' . esc_html( (string) $option ) . '</option>';
}
echo '</select>';
}
}
if ( function_exists( 'ppom_woocommerce_inputs_template_base' ) ) {
ppom_woocommerce_inputs_template_base();
}
echo '<button type="submit" class="single_add_to_cart_button button alt">' . esc_html__( 'Add to cart', 'woocommerce-product-addon' ) . '</button>';
echo '</form>';
echo '</div>';
echo '</main>';
wp_reset_postdata();
get_footer();
exit;
}
add_action( 'template_redirect', 'ppom_e2e_render_product_page_route', 0 );

/**
* Default license fixture: valid Essential plan (wp-env has no store key).
*
* @return array{status:string,plan:int}
* @return array{status:string,plan:int,pro_installed:bool}
*/
function ppom_e2e_default_license_fixture() {
return array(
'status' => 'valid',
'plan' => 1,
'status' => 'valid',
'plan' => 1,
'pro_installed' => false,
);
}

/**
* Resolved license fixture for filters and AJAX responses.
*
* @return array{status:string,plan:int}
* @return array{status:string,plan:int,pro_installed:bool}
*/
function ppom_e2e_get_license_fixture() {
$defaults = ppom_e2e_default_license_fixture();
Expand All @@ -56,15 +142,25 @@ function ppom_e2e_get_license_fixture() {
return $defaults;
}

$status = isset( $stored['status'] ) && 'invalid' === $stored['status'] ? 'invalid' : 'valid';
$plan = isset( $stored['plan'] ) ? max( 1, min( 3, absint( $stored['plan'] ) ) ) : $defaults['plan'];
$status = isset( $stored['status'] ) && 'invalid' === $stored['status'] ? 'invalid' : 'valid';
$plan = isset( $stored['plan'] ) ? max( 1, min( 3, absint( $stored['plan'] ) ) ) : $defaults['plan'];
$pro_installed = isset( $stored['pro_installed'] ) ? (bool) $stored['pro_installed'] : $defaults['pro_installed'];

return array(
'status' => $status,
'plan' => $plan,
'status' => $status,
'plan' => $plan,
'pro_installed' => $pro_installed,
);
}

if ( ppom_e2e_get_license_fixture()['pro_installed'] && ! class_exists( 'PPOM_PRO', false ) ) {
/**
* Minimal test double so wp-env can exercise premium-field admin UI gates
* without loading the separate PPOM Pro add-on.
*/
class PPOM_PRO {}
}

/**
* The attach modal only enables tag selection when the license filter returns valid.
* wp-env runs the free build without a store key; unlock valid for automated admin UI tests.
Expand Down Expand Up @@ -1104,13 +1200,15 @@ function ppom_e2e_set_license_fixture() {

$status_raw = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : '';
$plan_raw = isset( $_POST['plan'] ) ? absint( wp_unslash( $_POST['plan'] ) ) : 0;
$pro_raw = isset( $_POST['pro_installed'] ) ? sanitize_text_field( wp_unslash( $_POST['pro_installed'] ) ) : '';

$status = ( 'invalid' === $status_raw ) ? 'invalid' : 'valid';
$plan = max( 1, min( 3, $plan_raw > 0 ? $plan_raw : 1 ) );

$stored = array(
'status' => $status,
'plan' => $plan,
'status' => $status,
'plan' => $plan,
'pro_installed' => in_array( $pro_raw, array( '1', 'true', 'yes' ), true ),
);

update_option( PPOM_E2E_LICENSE_FIXTURE_OPTION, $stored, false );
Expand Down Expand Up @@ -1143,6 +1241,12 @@ function ppom_e2e_reset_state() {
ppom_e2e_require_capability();
ppom_e2e_require_nonce();

// Persistent wp-env volumes can lack or stale `personalizedproduct_db_version`, which
// makes bootstrap AJAX return db_version_outdated. Mirror plugin reactivation.
if ( class_exists( 'NM_PersonalizedProduct' ) ) {
NM_PersonalizedProduct::upgrade_database();
}

$deleted_meta_rows = 0;
$tracked_meta_ids = ppom_e2e_get_tracked_meta_ids();

Expand All @@ -1156,6 +1260,8 @@ function ppom_e2e_reset_state() {

delete_option( PPOM_E2E_META_IDS_OPTION );
delete_option( PPOM_E2E_LICENSE_FIXTURE_OPTION );
update_option( 'woocommerce_coming_soon', 'no', false );
update_option( 'woocommerce_store_pages_only', 'no', false );

if ( defined( 'PPOM_PRODUCT_META_KEY' ) ) {
delete_post_meta_by_key( PPOM_PRODUCT_META_KEY );
Expand Down
25 changes: 20 additions & 5 deletions classes/admin.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -598,9 +598,7 @@ public function get_wc_tags( $current_values ) {
* @return array
*/
public function get_db_field( $ppom_field_id ) {
$rows = ppom_meta_repository()->get_categories_and_tags_columns( (int) $ppom_field_id );

return ! empty( $rows ) ? $rows : array();
return \PPOM\Data\FieldGroupRepository::instance()->get_categories_tags_columns_by_id( absint( $ppom_field_id ) );
}

/**
Expand Down Expand Up @@ -1036,7 +1034,24 @@ public function ppom_attach_ppoms() {
* @return void
*/
public static function save_categories_and_tags( $ppom_id, $categories, $tags ) {
ppom_meta_repository()->save_categories_and_tags( (int) $ppom_id, $categories, $tags );
$data_to_update = array(
'productmeta_categories' => implode( "\r\n", $categories ), // NOTE: Keep the backward compatible format.
);

$format = array( '%s' );

// false = caller chose not to change tags (e.g. E2E partial update); omit column from UPDATE.
if ( is_array( $tags ) ) {
$data_to_update['productmeta_tags'] = empty( $tags ) ? '' : serialize( $tags );
$format[] = '%s';
}

\PPOM\Data\FieldGroupRepository::instance()->update_row(
$data_to_update,
array( 'productmeta_id' => $ppom_id ),
$format,
array( '%d' )
);
}

// Legacy settings bridge and setup.
Expand Down Expand Up @@ -1253,7 +1268,7 @@ public function set_legacy_user() {
return;
}

$res = ppom_meta_repository()->get_rows_with_non_empty_style_or_js();
$res = \PPOM\Data\FieldGroupRepository::instance()->find_rows_with_inline_js_or_style();
update_option( 'ppom_legacy_user', ! empty( $res ) ? 'yes' : 'no' );
}

Expand Down
35 changes: 32 additions & 3 deletions classes/fields.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';

// Preloader script
wp_enqueue_script( 'ppom-perload', PPOM_URL . '/js/admin/pre-load.js', array( 'jquery' ), PPOM_VERSION, true );

Check failure on line 76 in classes/fields.class.php

View workflow job for this annotation

GitHub Actions / PHPStan

Ignored error pattern #^Constant PPOM_URL not found\.$# (constant.notFound) in path /home/runner/work/woocommerce-product-addon/woocommerce-product-addon/classes/fields.class.php is expected to occur 22 times, but occurred 23 times.

// Bootstrap Files
wp_enqueue_style( 'ppom-bs', PPOM_URL . '/css/bootstrap/bootstrap.css' );
Expand All @@ -98,7 +98,7 @@
'invalid_pattern' => esc_html__( 'Range format is invalid. (range: {range})', 'woocommerce-product-addon' ),
),
),
)
)
);

wp_enqueue_script( 'ppom-inputmask', PPOM_URL . '/js/inputmask/jquery.inputmask.min.js', array( 'jquery' ), '5.0.6', true );
Expand Down Expand Up @@ -215,7 +215,7 @@
'wp-color-picker',
),
PPOM_VERSION,
true
true
);
wp_enqueue_script(
'ppom-preview-modal',
Expand Down Expand Up @@ -292,6 +292,35 @@
wp_localize_script( 'ppom-field', 'ppom_vars', $ppom_admin_meta );
wp_localize_script( 'ppom-meta-table', 'ppom_vars', $ppom_admin_meta );

$is_field_editor_screen = ( isset( $_GET['do_meta'] ) && 'edit' === $_GET['do_meta'] )
|| ( isset( $_GET['action'] ) && 'new' === $_GET['action'] );
if ( $is_field_editor_screen && function_exists( 'ppom_use_react_field_modal' ) && ppom_use_react_field_modal() ) {
$field_modal_asset = PPOM_PATH . '/packages/admin/field-modal/build/index.asset.php';
if ( file_exists( $field_modal_asset ) ) {
$field_modal = require $field_modal_asset;
wp_enqueue_script(
'ppom-admin-field-modal',
PPOM_URL . '/packages/admin/field-modal/build/index.js',

Check failure on line 303 in classes/fields.class.php

View workflow job for this annotation

GitHub Actions / PHPStan

Constant PPOM_URL not found.
$field_modal['dependencies'],
$field_modal['version'],
true
);
wp_set_script_translations(
'ppom-admin-field-modal',
'woocommerce-product-addon'
);
$productmeta_id = isset( $_GET['productmeta_id'] ) ? absint( $_GET['productmeta_id'] ) : 0;
wp_localize_script(
'ppom-admin-field-modal',
'ppomFieldModalBoot',
array(
'productmetaId' => $productmeta_id,
'nonce' => wp_create_nonce( 'wp_rest' ),
)
);
}
}

$action = isset( $_GET['action'] ) ? sanitize_text_field( $_GET['action'] ) : '';
$page_slug = 'fields';

Expand Down Expand Up @@ -1524,7 +1553,7 @@
$settings['min_img_w']['tabs_class'] = array( 'ppom_handle_image_dimension_tab', 'col-md-6' );
$settings['max_img_w']['tabs_class'] = array( 'ppom_handle_image_dimension_tab', 'col-md-6' );
$settings['img_dimension_error']['tabs_class'] = array( 'ppom_handle_image_dimension_tab', 'col-md-6' );
}
}
}

// ppom_pa
Expand Down
2 changes: 1 addition & 1 deletion classes/inputs/input.color.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private function get_settings() {
'desc' => __( 'Insert the error message for validation.', 'woocommerce-product-addon' ),
),
'default_color' => array(
'type' => 'text',
'type' => 'color',
'title' => __( 'Default color', 'woocommerce-product-addon' ),
'desc' => __( 'Define default color e.g: #effeff', 'woocommerce-product-addon' ),
'col_classes' => array( 'col-md-3', 'col-sm-12' ),
Expand Down
Loading
Loading