Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
338 changes: 260 additions & 78 deletions dt-contacts/duplicates-merging.php

Large diffs are not rendered by default.

40 changes: 39 additions & 1 deletion dt-core/admin/admin-enqueue-scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ function dt_options_scripts() {

if ( isset( $_GET['page'] ) && ( in_array( $_GET['page'], $allowed_pages, true ) ) ) {

// Normalize active tab so page=dt_options without tab param works (defaults to general)
$active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';

wp_register_script( 'jquery-ui-js', 'https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js', [ 'jquery' ], '1.12.1', true );
wp_enqueue_script( 'jquery-ui-js' );

Expand All @@ -90,14 +93,47 @@ 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' );

if ( isset( $_GET['tab'] ) && ( ( $_GET['tab'] === 'people-groups' ) || ( $_GET['tab'] === 'general' ) ) ) {
// Enqueue web components for dt-multi-select and other components (available on entire dt_options page)
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 ( ( $active_tab === 'people-groups' ) || ( $active_tab === 'general' ) ) {
wp_enqueue_script( 'dt_peoplegroups_scripts', get_template_directory_uri() . '/dt-people-groups/people-groups.js', [
'jquery',
'jquery-ui-core',
], filemtime( get_template_directory() . '/dt-people-groups/people-groups.js' ), true );
wp_localize_script( 'dt_peoplegroups_scripts', 'dtPeopleGroupsAPI', build_people_groups_api_object() );
}

// Field settings for all post types: expose under dtOptionAPI for use by duplicate-fields and other settings
$post_field_settings = [];
if ( $_GET['page'] === 'dt_options' ) {
$post_types = DT_Posts::get_post_types();
foreach ( $post_types as $post_type ) {
$post_field_settings[ $post_type ] = DT_Posts::get_post_field_settings( $post_type );
}
}

// Prepare duplicate fields data for general tab (read from database only; form processing is in tab-general.php)
$duplicate_fields_data = [
'config' => [],
'post_types' => [],
'fields' => [],
'defaults' => [],
];
if ( $active_tab === 'general' ) {
$site_options = dt_get_option( 'dt_site_options' );
$duplicates_config = $site_options['duplicates'] ?? [];
$duplicate_fields_data['config'] = $duplicates_config;
$duplicate_fields_data['post_types'] = array_values( DT_Posts::get_post_types() );
$defaults_data = [];
foreach ( $duplicate_fields_data['post_types'] as $post_type ) {
$defaults_data[ $post_type ] = dt_get_duplicate_fields_defaults( $post_type );
}
$duplicate_fields_data['fields'] = $post_field_settings;
$duplicate_fields_data['defaults'] = $defaults_data;
}

wp_localize_script(
'dt_options_script', 'dtOptionAPI', array(
'root' => esc_url_raw( rest_url() ),
Expand All @@ -109,6 +145,8 @@ 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' ),
'post_field_settings' => $post_field_settings,
'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
198 changes: 198 additions & 0 deletions dt-core/admin/js/dt-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -1528,4 +1528,202 @@ jQuery(document).ready(function ($) {
/**
* Storage Test Connection - [END]
*/

/**
* Duplicate Fields Configuration
*/
if (
window.dtOptionAPI &&
window.dtOptionAPI.duplicate_fields &&
window.dtOptionAPI.duplicate_fields.post_types &&
Array.isArray(window.dtOptionAPI.duplicate_fields.post_types) &&
window.dtOptionAPI.duplicate_fields.post_types.length > 0 &&
$('#duplicate-fields-form').length > 0
) {
// Safe config: ensure object (avoid accessing from prototype)
const duplicateFieldsConfig =
window.dtOptionAPI.duplicate_fields.config != null &&
typeof window.dtOptionAPI.duplicate_fields.config === 'object' &&
!Array.isArray(window.dtOptionAPI.duplicate_fields.config)
? window.dtOptionAPI.duplicate_fields.config
: {};
// Ensure post_types is an array (handle case where it might be an object due to non-sequential keys)
const postTypesRaw = window.dtOptionAPI.duplicate_fields.post_types || [];
const allPostTypes = Array.isArray(postTypesRaw)
? postTypesRaw
: Object.values(postTypesRaw);
// Prefer shared post_field_settings; fallback to duplicate_fields.fields for backward compatibility
const fieldsData =
window.dtOptionAPI.post_field_settings &&
typeof window.dtOptionAPI.post_field_settings === 'object'
? window.dtOptionAPI.post_field_settings
: window.dtOptionAPI.duplicate_fields.fields || {};
const defaultFields = window.dtOptionAPI.duplicate_fields.defaults || {};

// Transform field settings to dt-multi-select format
// Note: These types must match those supported in extract_field_values_for_duplicate_search()
// in dt-contacts/duplicates-merging.php
function transformFieldsForMultiSelect(fields) {
const allowedTypes = [
'text',
'textarea',
'number',
'communication_channel',
'tags',
'multi_select',
];

const fieldsArray = Object.keys(fields || {}).map(function (fieldKey) {
const field = fields[fieldKey];
return {
key: fieldKey,
name: field.name || fieldKey,
type: field.type,
hidden: field.hidden || false,
private: field.private || false,
icon: field.icon || field['font-icon'] || null,
};
});

return fieldsArray
.filter(function (field) {
return (
!field.hidden && !field.private && allowedTypes.includes(field.type)
);
})
.sort(function (a, b) {
const nameA = (a.name || '').toLowerCase();
const nameB = (b.name || '').toLowerCase();
return nameA.localeCompare(nameB);
})
.map(function (field) {
return {
id: field.key,
label: field.name || field.key,
color: null,
icon: field.icon || null,
};
});
}

// Update field selector with fields
function updateFieldSelector(postType) {
const fieldSelector = document.querySelector(
'#duplicate_fields_selector',
);
if (!fieldSelector) return;

const fields = fieldsData[postType] || {};
const options = transformFieldsForMultiSelect(fields);
fieldSelector.options = options;

// Set selected values from config, or use defaults if no valid config exists
// Check if valid config exists for this post type (use Object.prototype.hasOwnProperty.call to avoid unsafe hasOwnProperty from target object)
let selectedFields;
if (
Object.prototype.hasOwnProperty.call(duplicateFieldsConfig, postType) &&
Array.isArray(duplicateFieldsConfig[postType]) &&
duplicateFieldsConfig[postType].length > 0
) {
// Valid saved config exists - use it
selectedFields = duplicateFieldsConfig[postType];
} else {
// No valid config exists - use defaults
selectedFields = defaultFields[postType] || [];
}

// Filter selectedFields to only include fields that exist in options
// This prevents errors when a field was configured but later removed or filtered out
const availableOptionIds = options.map(function (opt) {
return opt.id;
});
const validSelectedFields = selectedFields.filter(function (fieldId) {
return availableOptionIds.includes(fieldId);
});

fieldSelector.value = validSelectedFields;
}

// Handle post type change
$('#duplicate_fields_post_type').on('change', function () {
const postType = $(this).val();
updateFieldSelector(postType);
});

// Handle form submission
$('#duplicate-fields-form').on('submit', function (e) {
e.preventDefault();

const postType = $('#duplicate_fields_post_type').val();
const fieldSelector = document.querySelector(
'#duplicate_fields_selector',
);

if (!fieldSelector) {
alert('Field selector not found');
return false;
}

// Collect all post type configurations
const allConfigs = {};

// Get current selection - always save current selection, even if empty
// (empty will be handled by PHP to use defaults)
const currentFields = Array.isArray(fieldSelector.value)
? fieldSelector.value
: [];

// Always save the current post type selection (even if empty array)
// PHP will ensure 'name' is included and handle empty arrays
allConfigs[postType] = currentFields;

// Include other post types from existing config
Object.keys(duplicateFieldsConfig).forEach(function (pt) {
if (pt !== postType && duplicateFieldsConfig[pt]) {
allConfigs[pt] = duplicateFieldsConfig[pt];
}
});

// Set hidden input
$('#duplicate_fields_data').val(JSON.stringify(allConfigs));

// Submit form
this.submit();
});

// Initialize on page load - wait for web component to be defined
function initializeDuplicateFields() {
const fieldSelector = document.querySelector(
'#duplicate_fields_selector',
);
const postTypeSelect = $('#duplicate_fields_post_type');

if (!fieldSelector || !postTypeSelect.length) {
return;
}

// Check if component is defined
if (
window.customElements &&
window.customElements.get('dt-multi-select')
) {
// Component is ready, initialize with the currently selected post type
const selectedPostType = postTypeSelect.val();
if (selectedPostType && fieldsData[selectedPostType]) {
updateFieldSelector(selectedPostType);
} else if (allPostTypes.length > 0) {
// Fallback to first post type if no selection
updateFieldSelector(allPostTypes[0]);
}
} else {
// Wait for component to be defined
setTimeout(initializeDuplicateFields, 100);
}
}

// Start initialization
$(document).ready(function () {
initializeDuplicateFields();
});
}
});
Loading