diff --git a/src/SettingsTrait.php b/src/SettingsTrait.php index 7270b18..eeb1252 100644 --- a/src/SettingsTrait.php +++ b/src/SettingsTrait.php @@ -3,23 +3,41 @@ /** * Use overloading magic methods for short syntax. + * Property accesses are recorded so unparse_custom_settings() can detect + * which keys were touched (= natively handled) vs untouched (= potentially custom). */ trait SettingsTrait { protected $settings; + /** + * Keys accessed via __get / __set / __isset during this object's lifetime. + * Stored as a hash-map (key => true) for O(1) lookup. + * + * @var array + */ + private $accessed_keys = []; + public function get_settings(): array { return $this->settings; } + /** Return all keys that were accessed (touched) so far. */ + public function get_accessed_keys(): array { + return $this->accessed_keys; + } + public function __get( string $key ) { + $this->accessed_keys[ $key ] = true; return $this->settings[ $key ] ?? null; } public function __set( string $key, $value ): void { + $this->accessed_keys[ $key ] = true; $this->settings[ $key ] = $value; } public function __isset( string $key ): bool { + $this->accessed_keys[ $key ] = true; return isset( $this->settings[ $key ] ); } diff --git a/src/Unparsers/Base.php b/src/Unparsers/Base.php index 44fa595..7640598 100644 --- a/src/Unparsers/Base.php +++ b/src/Unparsers/Base.php @@ -135,21 +135,44 @@ protected function unparse_conditional_logic() { $output = []; foreach ( $conditional_logic as $action => $condition ) { + // Fix: chỉ xử lý action đầu tiên ('visible' được ưu tiên hơn 'hidden'). + // Vòng lặp cũ reset $output['when'] mỗi lần, khiến action trước bị mất. $output['type'] = $action; $output['relation'] = $condition['relation']; $output['when'] = []; foreach ( $condition['when'] as $criteria ) { - $name = $criteria[0]; // Use field name as key + // Fix: dùng uniqid() làm array key thay vì $name. + // Nếu cùng field xuất hiện nhiều lần (toán tử khác nhau), + // dùng $name làm key khiến rule sau ghi đè rule trước. + $uid = uniqid(); + + // Fix: convert array value thành CSV string. + // Parser/Base.php dùng Arr::from_csv() để chuyển "5,6,7" → [5,6,7]. + // Chiều ngược lại (Unparser), phải convert [5,6,7] → "5,6,7" + // để Builder UI hiển thị đúng — vd: 'in' operator với tax_input. + $value = $criteria[2] ?? ''; + if ( is_array( $value ) ) { + $value = implode( ',', $value ); + } - $output['when'][ $name ] = [ - 'id' => $name, - 'name' => $name, + $output['when'][ $uid ] = [ + 'id' => $uid, + 'name' => $criteria[0], 'operator' => $criteria[1], - 'value' => $criteria[2], + 'value' => $value, ]; } + + // Chỉ lấy action đầu tiên, thoát vòng lặp. + break; } + + // Fix: đánh dấu 'visible' và 'hidden' là đã được xử lý (accessed) + // trước khi unset, để unparse_custom_settings() không nhặt chúng lên + // và nhét vào custom_settings một cách sai lầm. + $this->accessed_keys['visible'] = true; + $this->accessed_keys['hidden'] = true; unset( $this->visible, $this->hidden ); $this->conditional_logic = $output; diff --git a/src/Unparsers/Field.php b/src/Unparsers/Field.php index 124b68f..5910033 100644 --- a/src/Unparsers/Field.php +++ b/src/Unparsers/Field.php @@ -10,6 +10,7 @@ class Field extends Base { private $choice_types = [ 'select', 'radio', 'checkbox_list', 'select_advanced', 'button_group', 'image_select', 'autocomplete' ]; + /** * This is revert of parse method. While parse method converts to the minimal format, * this method converts back to the original format. @@ -33,13 +34,17 @@ public function unparse() { ->unparse_text_limiter() ->unparse_conditional_logic() ->unparse_tooltip() - ->unparse_admin_columns() - ->ensure_boolean( 'save_field' ); + ->unparse_admin_columns(); + // Field-type handler must run BEFORE unparse_custom_settings() so its + // property accesses are tracked and not mis-classified as custom attributes. $func = "unparse_field_{$this->type}"; if ( method_exists( $this, $func ) ) { $this->$func(); } + + $this->unparse_custom_settings(); + $this->ensure_boolean( 'save_field' ); } private function unparse_datalist() { @@ -140,15 +145,19 @@ private function unparse_field_group() { } private function unparse_text_limiter() { - if ( ! isset( $this->limit ) ) { + // Accept fields that only export limit_type (limit=0 is stripped on export as default). + if ( ! isset( $this->limit ) && ! isset( $this->limit_type ) ) { return $this; } $this->text_limiter = [ - 'limit' => $this->limit, + 'limit' => $this->limit ?? 0, 'limit_type' => $this->limit_type ?? 'word', ]; + // Remove flat keys so they are not double-stored alongside text_limiter. + unset( $this->settings['limit'], $this->settings['limit_type'] ); + return $this; } @@ -206,6 +215,59 @@ private function unparse_admin_columns() { return $this; } + /** + * Pure pass-through keys that no unparse_* method ever reads via $this->key. + * They are kept in settings as-is, so they must never be classified as + * user-defined custom attributes. + * + * Every other native key is auto-tracked by SettingsTrait the moment any + * method does $this->key — no need to list them here. + * + * Extend via filter: + * add_filter( 'mbb_parser_known_field_keys', fn( $k ) => array_merge( $k, [ 'my_key' ] ) ); + */ + private const STRUCTURAL_KEYS = [ + 'type', 'id', '_id', 'name', 'std', 'placeholder', + 'columns', 'field_name', '_state', 'input_attributes', + 'multiple', 'fields', 'tab', 'validation', + 'custom_settings', '_callback', 'options', + ]; + + /** + * Detect unknown top-level keys and move them into the custom_settings + * format `{ uid: { id, key, value } }` that the Builder UI renders in + * the "Custom Settings" tab. + * + * "Known" = auto-tracked by SettingsTrait + STRUCTURAL_KEYS pass-through list. + */ + private function unparse_custom_settings(): self { + $always_native = apply_filters( 'mbb_parser_known_field_keys', self::STRUCTURAL_KEYS ); + $known = $this->get_accessed_keys() + array_flip( $always_native ); + $custom = $this->custom_settings ?? []; + + foreach ( array_diff_key( $this->settings, $known ) as $key => $value ) { + if ( is_resource( $value ) ) { + continue; + } + + $uid = uniqid(); + $custom[ $uid ] = [ + 'id' => $uid, + 'key' => $key, + 'value' => ( is_array( $value ) || is_object( $value ) ) + ? wp_json_encode( $value ) + : (string) $value, + ]; + unset( $this->settings[ $key ] ); + } + + if ( ! empty( $custom ) ) { + $this->custom_settings = $custom; + } + + return $this; + } + public function unparse_default_values() { $this->id = $this->id ?? uniqid(); $this->_id = $this->_id ?? $this->id;