Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e46334f
Enhance modular-list.js with Foundation checks
kodinkat Feb 5, 2026
75fe35d
Implement duplicate detection configuration in admin settings
kodinkat Feb 5, 2026
779840b
Refactor JavaScript for improved Foundation integration
kodinkat Feb 5, 2026
0a1b287
Refactor duplicate fields configuration handling in admin settings
kodinkat Feb 5, 2026
73f392b
Refactor duplicate fields handling in global functions and admin scripts
kodinkat Feb 5, 2026
d5db319
Fix HTML structure in general settings tab by correcting option tag i…
kodinkat Feb 5, 2026
247d97b
Improve HTML structure in general settings tab by correcting option t…
kodinkat Feb 5, 2026
a4ad132
Refine duplicate fields configuration text in general settings tab fo…
kodinkat Feb 5, 2026
67a800b
Refactor Foundation integration in modular-list.js and contacts.js
kodinkat Feb 11, 2026
6f5f659
Merge branch 'develop' of https://github.com/DiscipleTools/disciple-t…
kodinkat Feb 11, 2026
d7e2b68
Refactor duplicate fields handling in admin scripts
kodinkat Feb 11, 2026
3236b57
Merge branch 'develop' of https://github.com/DiscipleTools/disciple-t…
kodinkat Feb 25, 2026
a8efb51
Refactor duplicate fields logic and enhance admin scripts
kodinkat Feb 25, 2026
9b483af
Refactor duplicate fields handling and streamline admin scripts
kodinkat Feb 25, 2026
bf37fd9
Refactor SQL query construction in duplicate merging logic
kodinkat Feb 25, 2026
42d2e85
Enhance SQL query handling in duplicate merging logic
kodinkat Feb 25, 2026
2d3f77f
add tests, no fuzzy search on tags and multi_select
corsacca Mar 2, 2026
f2d44d1
add group tests
corsacca Mar 2, 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
128 changes: 110 additions & 18 deletions dt-contacts/duplicates-merging.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,35 +95,127 @@ public function get_ids_of_non_dismissed_duplicates_endpoint( WP_REST_Request $r
}


/**
* Extract field values for duplicate search based on field type
*
* @param array $post The post data array
* @param string $field_key The field key to extract
* @param array $field_settings The field settings array
* @param string $exact_template The exact match template (e.g., '^' for exact, '' for fuzzy)
* @return array|null Array of search values or null if no values found
*/
private static function extract_field_values_for_duplicate_search( $post, $field_key, $field_settings, $exact_template ){
if ( !isset( $field_settings['type'] ) || empty( $field_settings['type'] ) ){
return null;
}

$field_type = $field_settings['type'];
$field_value = $post[$field_key] ?? null;

if ( empty( $field_value ) ){
return null;
}

$search_values = [];

switch ( $field_type ) {
case 'text':
case 'textarea':
case 'number':
// Direct string/number value
$search_values[] = $exact_template . $field_value;
break;

case 'communication_channel':
// Array of objects with 'value' key
if ( is_array( $field_value ) ){
foreach ( $field_value as $value ){
if ( !empty( $value['value'] ) ){
$search_values[] = $exact_template . $value['value'];
}
}
}
break;

case 'tags':
// Array of tag values
if ( is_array( $field_value ) ){
foreach ( $field_value as $tag ){
$tag_value = is_array( $tag ) ? ( $tag['value'] ?? $tag['label'] ?? '' ) : $tag;
if ( !empty( $tag_value ) ){
$search_values[] = $exact_template . $tag_value;
}
}
}
break;

case 'multi_select':
// Array of selected keys
if ( is_array( $field_value ) ){
foreach ( $field_value as $selected ){
$selected_value = is_array( $selected ) ? ( $selected['key'] ?? $selected['value'] ?? '' ) : $selected;
if ( !empty( $selected_value ) ){
$search_values[] = $exact_template . $selected_value;
}
}
}
break;

default:
// For other types, try to extract as string
if ( is_scalar( $field_value ) ){
$search_values[] = $exact_template . $field_value;
} else if ( is_array( $field_value ) && isset( $field_value['value'] ) ){
$search_values[] = $exact_template . $field_value['value'];
}
break;
}

return !empty( $search_values ) ? $search_values : null;
}

private static function query_for_duplicate_searches( $post_type, $post_id, $exact = true ){
$post = DT_Posts::get_post( $post_type, $post_id );
$fields = DT_Posts::get_post_field_settings( $post_type );
$search_query = [];
$exact_template = $exact ? '^' : '';
$fields_with_values = [];
foreach ( $post as $field_key => $field_value ){
if ( ! isset( $fields[$field_key]['type'] ) || empty( $fields[$field_key]['type'] ) ){

// Get configured duplicate fields, or use defaults if not configured
$site_options = dt_get_option( 'dt_site_options' );
$duplicates_config = $site_options['duplicates'] ?? [];

// Check if valid configuration exists for this post type
// Use saved config if it exists and is not empty, otherwise use defaults
if ( isset( $duplicates_config[$post_type] ) && !empty( $duplicates_config[$post_type] ) ) {
// Use saved configuration
$configured_fields = $duplicates_config[$post_type];
} else {
// No valid configuration exists - use defaults
$configured_fields = dt_get_duplicate_fields_defaults( $post_type );
}

// Process each configured field
foreach ( $configured_fields as $field_key ){
// Skip if field doesn't exist in post or field settings
if ( !isset( $post[$field_key] ) || !isset( $fields[$field_key] ) ){
continue;
}
if ( $fields[$field_key]['type'] === 'communication_channel' ){
if ( !empty( $field_value ) ){
$channel_queries = [];
foreach ( $field_value as $value ){
if ( !empty( $value['value'] ) ){
$channel_queries[] = $exact_template . $value['value'];
}
}
if ( !empty( $channel_queries ) ){
$fields_with_values[] = $field_key;
$search_query[$field_key] = [];
$search_query[$field_key] = $channel_queries;
}
}
} else if ( $field_key === 'name' && !empty( $field_value ) ){

// Extract values based on field type
$search_values = self::extract_field_values_for_duplicate_search(
$post,
$field_key,
$fields[ $field_key ],
$exact_template
);

if ( !empty( $search_values ) ){
$fields_with_values[] = $field_key;
$search_query[$field_key] = [ $exact_template . $field_value ];
$search_query[ $field_key ] = $search_values;
}
}

return [
'query' => $search_query,
'fields' => $fields_with_values,
Expand Down
86 changes: 86 additions & 0 deletions dt-core/admin/admin-enqueue-scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ function dt_options_scripts() {
dt_theme_enqueue_style( 'material-font-icons-local', 'dt-core/dependencies/mdi/css/materialdesignicons.min.css', array() );
wp_enqueue_style( 'material-font-icons', 'https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css' );

// Enqueue web components for dt-multi-select and other components
dt_theme_enqueue_script( 'web-components', 'dt-assets/build/components/index.js', array(), false );
dt_theme_enqueue_style( 'web-components-css', 'dt-assets/build/css/light.min.css', array() );

if ( isset( $_GET['tab'] ) && ( ( $_GET['tab'] === 'people-groups' ) || ( $_GET['tab'] === 'general' ) ) ) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this page need to work on /wp-admin/admin.php?page=dt_options as well.

wp_enqueue_script( 'dt_peoplegroups_scripts', get_template_directory_uri() . '/dt-people-groups/people-groups.js', [
'jquery',
Expand All @@ -98,6 +102,87 @@ function dt_options_scripts() {
wp_localize_script( 'dt_peoplegroups_scripts', 'dtPeopleGroupsAPI', build_people_groups_api_object() );
}

// Prepare duplicate fields data for general tab
// Initialize as object (associative array) to ensure consistent structure
$duplicate_fields_data = [
'config' => [],
'post_types' => [],
'fields' => [],
'defaults' => [],
];
if ( isset( $_GET['tab'] ) && $_GET['tab'] === 'general' ) {
// Check if we're processing a duplicate fields form submission
// If so, read from POST data to get the latest values before they're saved
$duplicates_config = null; // Use null to distinguish "no POST data" from "empty config"
$has_post_data = false;

if ( isset( $_POST['duplicate_fields_nonce'] ) &&
wp_verify_nonce( sanitize_key( wp_unslash( $_POST['duplicate_fields_nonce'] ) ), 'duplicate_fields' ) &&
isset( $_POST['duplicate_fields_data'] ) && !empty( $_POST['duplicate_fields_data'] ) ) {
// Form is being submitted - read from POST to get the latest data
$has_post_data = true;
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON data will be sanitized after decoding
$decoded_data = json_decode( wp_unslash( $_POST['duplicate_fields_data'] ), true );
if ( is_array( $decoded_data ) ) {
$duplicates_config = [];
$post_types = DT_Posts::get_post_types();
foreach ( $decoded_data as $post_type => $fields ) {
if ( in_array( $post_type, $post_types ) && is_array( $fields ) ) {
// Handle both empty arrays (user cleared all fields) and non-empty arrays
if ( !empty( $fields ) ) {
// Non-empty array - sanitize and save
$sanitized_fields = [];
foreach ( $fields as $field_key ) {
$sanitized_field_key = sanitize_key( $field_key );
$field_settings = DT_Posts::get_post_field_settings( $post_type );
if ( isset( $field_settings[$sanitized_field_key] ) ) {
$sanitized_fields[] = $sanitized_field_key;
}
}
if ( !empty( $sanitized_fields ) ) {
$duplicates_config[$post_type] = array_unique( $sanitized_fields );
}
// If sanitized_fields is empty (all invalid), don't add to config (use defaults)
}
// Empty array means user wants to revert to defaults - don't set the key
}
}
}
}

// If we didn't get data from POST, read from database
if ( !$has_post_data ) {
// Clear cache to ensure we read fresh data
wp_cache_delete( 'dt_site_options', 'options' );
wp_cache_delete( 'alloptions', 'options' );
$site_options = dt_get_option( 'dt_site_options' );
$duplicates_config = $site_options['duplicates'] ?? [];
} else {
// We have POST data - use it (even if empty array, which means use defaults)
// $duplicates_config is already set above
if ( $duplicates_config === null ) {
$duplicates_config = [];
}
}

$duplicate_fields_data['config'] = $duplicates_config;
// Use array_values() to ensure sequential array keys (0, 1, 2...) for proper JSON encoding as array
$duplicate_fields_data['post_types'] = array_values( DT_Posts::get_post_types() );

// Pre-load field settings and defaults for all post types
$fields_data = [];
$defaults_data = [];
foreach ( $duplicate_fields_data['post_types'] as $post_type ) {
$field_settings = DT_Posts::get_post_field_settings( $post_type );
$fields_data[$post_type] = $field_settings;

// Get default fields for this post type
$defaults_data[$post_type] = dt_get_duplicate_fields_defaults( $post_type );
}
$duplicate_fields_data['fields'] = $fields_data;
$duplicate_fields_data['defaults'] = $defaults_data;
}

wp_localize_script(
'dt_options_script', 'dtOptionAPI', array(
'root' => esc_url_raw( rest_url() ),
Expand All @@ -109,6 +194,7 @@ function dt_options_scripts() {
'available_languages' => dt_get_available_languages(),
'site_options' => dt_get_option( 'dt_site_options' ),
'contacts_field_settings' => DT_Posts::get_post_field_settings( 'contacts' ),
'duplicate_fields' => $duplicate_fields_data,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kodinkat the field_settings for all post types could be directly available under dtOptionAPI. Then it can be used by other settings

)
);
wp_register_style( 'dt_admin_css', disciple_tools()->admin_css_url . 'disciple-tools-admin-styles.css', [], filemtime( disciple_tools()->admin_css_path . 'disciple-tools-admin-styles.css' ) );
Expand Down
Loading
Loading