From 4266a5abccb334f73647c1779f403fa46b4e5bb0 Mon Sep 17 00:00:00 2001 From: kodinkat Date: Mon, 26 Jan 2026 16:19:00 +0000 Subject: [PATCH 01/10] Enhance bulk edit functionality: Add synthetic 'Share' and 'Follow' fields with typeahead support for user selection. Implement special handling for these fields in the bulk edit process, including payload adjustments for sharing actions. Ensure backward compatibility with legacy share input handling. --- dt-assets/js/modular-list.js | 399 +++++++++++++++++++++++++++++++++-- 1 file changed, 385 insertions(+), 14 deletions(-) diff --git a/dt-assets/js/modular-list.js b/dt-assets/js/modular-list.js index 93524bd070..7056c6b82c 100644 --- a/dt-assets/js/modular-list.js +++ b/dt-assets/js/modular-list.js @@ -1125,6 +1125,22 @@ icon: null, }); + // Add synthetic "Share" option (not a real post field) + transformedFields.push({ + id: 'share', + label: window.wpApiShare?.translations?.share || 'Share', + color: null, + icon: null, + }); + + // Add synthetic "Follow" option (not a real post field) + transformedFields.push({ + id: 'follow', + label: window.wpApiShare?.translations?.follow || 'Follow', + color: null, + icon: null, + }); + return transformedFields; } @@ -1324,6 +1340,43 @@ $(), // No icon for comments ); } + } else if (fieldKey === 'share') { + // Handle special 'share' field (not a real post field) + if (!bulkEditSelectedFields.some((f) => f.fieldKey === fieldKey)) { + bulkEditSelectedFields.push({ + fieldKey: fieldKey, + fieldType: 'share', + fieldName: window.wpApiShare?.translations?.share || 'Share', + cleared: false, + shareUserId: null, // Store selected user ID for clear/restore + }); + + // Render the share field + renderBulkEditField( + fieldKey, + 'share', + window.wpApiShare?.translations?.share || 'Share', + $(), // No icon for share + ); + } + } else if (fieldKey === 'follow') { + // Handle special 'follow' field (not a real post field) + if (!bulkEditSelectedFields.some((f) => f.fieldKey === fieldKey)) { + bulkEditSelectedFields.push({ + fieldKey: fieldKey, + fieldType: 'follow', + fieldName: window.wpApiShare?.translations?.follow || 'Follow', + cleared: false, + }); + + // Render the follow field + renderBulkEditField( + fieldKey, + 'follow', + window.wpApiShare?.translations?.follow || 'Follow', + $(), // No icon for follow + ); + } } else { // Handle regular post fields const fieldData = window.post_type_fields[fieldKey]; @@ -1426,7 +1479,7 @@ const inputContainer = wrapper.find('.bulk-edit-field-input-container'); renderBulkEditFieldInput(fieldKey, fieldType, inputContainer); - // Show clear button for fields that support clearing (exclude comment fields) + // Show clear button for fields that support clearing (exclude comment, follow, and share fields) if (supportsFieldClearing(fieldType) && fieldType !== 'comment') { wrapper.find('.bulk-edit-clear-field-btn').show(); } @@ -1594,6 +1647,173 @@ return; } + // Handle share field specially + if (fieldType === 'share') { + const shareInputId = `bulk_share_${fieldKey}`; + const shareResultContainerId = `bulk_share-result-container_${fieldKey}`; + + // Build share HTML with typeahead input + let shareHtml = '
'; + shareHtml += + ''; + shareHtml += + '
'; + shareHtml += '
'; + shareHtml += '
'; + shareHtml += ''; + shareHtml += + ''; + shareHtml += ''; + shareHtml += '
'; + shareHtml += '
'; + shareHtml += '
'; + shareHtml += '
'; + + container.html(shareHtml); + + // Initialize typeahead for share field + requestAnimationFrame(() => { + const shareInput = $(`.js-typeahead-${shareInputId}`); + if (shareInput.length === 0) { + return; + } + + // Get field data to check if we need to restore a previous user selection + const fieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + const previousUserId = fieldData?.shareUserId || null; + + // Initialize typeahead (adapted from shared-functions.js share typeahead) + const typeaheadSelector = `.js-typeahead-${shareInputId}`; + window.Typeahead[typeaheadSelector] = jQuery.typeahead({ + input: typeaheadSelector, + minLength: 0, + maxItem: 0, + accent: true, + searchOnFocus: true, + template: function (query, item) { + return `
+
+ + {{name}} (#${window.SHAREDFUNCTIONS.escapeHTML(item.ID)}) +
+
`; + }, + source: window.TYPEAHEADS.typeaheadUserSource(), + emptyTemplate: window.SHAREDFUNCTIONS.escapeHTML( + window.wpApiShare?.translations?.no_records_found || + 'No records found', + ), + display: 'name', + templateValue: '{{name}}', + dynamic: true, + callback: { + onClick: function (node, a, item, event) { + // Get fresh reference to fieldData from the array (closure might have stale reference) + const currentFieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + + // Store selected user ID in fieldData + if (currentFieldData) { + currentFieldData.shareUserId = item.ID; + } + // Store in input data for easy retrieval + shareInput.data('selected-user-id', item.ID); + shareInput.data('selected-user-name', item.name); + // Add visual feedback + shareInput.attr('data-selected', 'true'); + // Update input value to show selected user + shareInput.val(item.name); + + // Prevent default and hide layout + event.preventDefault(); + this.hideLayout(); + }, + onResult: function (node, query, result, resultCount) { + if (query) { + let text = window.TYPEAHEADS.typeaheadHelpText( + resultCount, + query, + result, + ); + $(`#${shareResultContainerId}`).html(text); + } + }, + onHideLayout: function () { + $(`#${shareResultContainerId}`).html(''); + }, + }, + }); + + // If we have a previous user ID (from restore), try to pre-select it + if (previousUserId) { + // Note: This would require fetching user data, which might be complex + // For now, we'll just restore the user_id in fieldData + // The user will need to re-select if they want to see it visually + if (fieldData) { + fieldData.shareUserId = previousUserId; + } + shareInput.data('selected-user-id', previousUserId); + } + }); + + return; + } + + // Handle follow field specially + if (fieldType === 'follow') { + const followToggleId = `bulk_follow_${fieldKey}`; + + // Build follow HTML with dt-toggle component + let followHtml = '
'; + followHtml += + ''; + followHtml += '
'; + + container.html(followHtml); + + // Initialize toggle value (default to false - not following) + requestAnimationFrame(() => { + const toggleComponent = document.getElementById(followToggleId); + if (toggleComponent) { + // Set default value to false (not following) + // dt-toggle component uses 'checked' property + if (toggleComponent.checked !== undefined) { + toggleComponent.checked = false; + } else if (toggleComponent.value !== undefined) { + // Fallback: some implementations might use value property + toggleComponent.value = false; + } else { + // Set internal input element if component doesn't expose checked directly + const internalInput = toggleComponent.querySelector( + 'input[type="checkbox"]', + ); + if (internalInput) { + internalInput.checked = false; + } + } + // Optional: Check current user's follow state if available + // For now, default to false + } + }); + + return; + } + // Get field settings from list_settings const fieldSettings = window.lodash.get( list_settings, @@ -3880,7 +4100,34 @@ } let updatePayload = {}; - let sharePayload; + let sharePayload = null; + + // Check for share field in dynamically selected fields + const shareFieldSelected = bulkEditSelectedFields?.some( + (f) => f.fieldKey === 'share', + ); + if (shareFieldSelected) { + const shareFieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === 'share', + ); + if (shareFieldData) { + // Collect user ID from input + const shareFieldWrapper = $( + `.bulk-edit-field-wrapper[data-field-key="share"]`, + ); + const shareUserId = collectFieldValue( + 'share', + 'share', + shareFieldWrapper, + ); + if (shareUserId) { + sharePayload = { + user_id: shareUserId, + action: 'add', + }; + } + } + } // Process web component values const form = document.getElementById('bulk_edit_picker'); @@ -3997,8 +4244,14 @@ const fieldKey = fieldData.fieldKey; const fieldType = fieldData.fieldType; + // Skip share field - it's handled separately via sharePayload + if (fieldType === 'share') { + return; + } + // Skip if field is marked as cleared - set cleared value and ensure it's not overwritten - if (fieldData.cleared) { + // Note: follow field doesn't support clearing (users toggle between states) + if (fieldData.cleared && fieldType !== 'follow') { const clearedValue = getClearedFieldValue(fieldType); // For communication_channel, backend expects: {"contact_email":[],"force_values":true} // We need to create an object that has array-like structure with force_values property @@ -4025,8 +4278,16 @@ const fieldValue = collectFieldValue(fieldKey, fieldType, fieldWrapper); if (fieldValue !== null && fieldValue !== undefined) { + // Handle follow field specially - add both follow and unfollow to payload + if (fieldType === 'follow') { + // Follow field returns { follow: {...}, unfollow: {...} } structure + if (fieldValue.follow && fieldValue.unfollow) { + updatePayload['follow'] = fieldValue.follow; + updatePayload['unfollow'] = fieldValue.unfollow; + } + } // Handle communication_channel fields specially (direct array format, not wrapped) - if (fieldType === 'communication_channel') { + else if (fieldType === 'communication_channel') { // Communication channel expects direct array: [{"value":"...","key":"..."}] if (Array.isArray(fieldValue)) { updatePayload[fieldKey] = fieldValue; @@ -4067,17 +4328,20 @@ }); } + // Legacy share input handling (for backward compatibility) shareInput.each(function () { - sharePayload = $(this).data('bulk_key_share'); + const legacySharePayload = $(this).data('bulk_key_share'); + // Only use legacy sharePayload if new sharePayload is not set + if (!sharePayload && legacySharePayload) { + sharePayload = { + users: legacySharePayload, + unshare: $('#bulk_share_unshare').length + ? $('#bulk_share_unshare').prop('checked') + : false, + }; + } }); - let shares = { - users: sharePayload, - unshare: $('#bulk_share_unshare').length - ? $('#bulk_share_unshare').prop('checked') - : false, - }; - let queue = []; let count = 0; $('.bulk_edit_checkbox input').each(function () { @@ -4095,7 +4359,7 @@ do_each, do_done, updatePayload, - shares, + sharePayload, commentPayload, ); } else { @@ -4125,6 +4389,100 @@ return null; } + // Special case: share field (not a real post field type) + if (fieldType === 'share') { + const shareInputId = `bulk_share_${fieldKey}`; + const shareInput = fieldWrapper.find(`.js-typeahead-${shareInputId}`); + + if (shareInput.length > 0) { + // Get selected user ID from input data + let selectedUserId = shareInput.data('selected-user-id'); + + // Fallback: check typeahead instance + if (!selectedUserId) { + const typeaheadSelector = `.js-typeahead-${shareInputId}`; + const typeaheadInstance = window.Typeahead?.[typeaheadSelector]; + if ( + typeaheadInstance && + typeaheadInstance.items && + typeaheadInstance.items.length > 0 + ) { + selectedUserId = typeaheadInstance.items[0].ID; + } + } + + // Also store in fieldData for later use + const fieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + if (fieldData && selectedUserId) { + fieldData.shareUserId = selectedUserId; + } + + return selectedUserId || null; + } + return null; + } + + // Special case: follow field (not a real post field type) + if (fieldType === 'follow') { + const followToggleId = `bulk_follow_${fieldKey}`; + const toggleComponent = + fieldWrapper.find(`#${followToggleId}`)[0] || + document.getElementById(followToggleId); + + if (toggleComponent) { + // Get toggle value (true/false) + // dt-toggle component may expose 'value' property (for ComponentService compatibility) + // or 'checked' property, or we need to check internal input element + let toggleValue = false; + + // First, try to get value property (ComponentService expects this) + if ( + toggleComponent.value !== undefined && + toggleComponent.value !== null + ) { + if (typeof toggleComponent.value === 'boolean') { + toggleValue = toggleComponent.value === true; + } else if (typeof toggleComponent.value === 'string') { + toggleValue = toggleComponent.value.toLowerCase() === 'true'; + } else { + toggleValue = Boolean(toggleComponent.value); + } + } + // Fallback to checked property + else if (toggleComponent.checked !== undefined) { + toggleValue = toggleComponent.checked === true; + } + // Last resort: check internal input element + else { + const internalInput = toggleComponent.querySelector( + 'input[type="checkbox"]', + ); + if (internalInput) { + toggleValue = internalInput.checked === true; + } + } + + // Get current user ID + const currentUserId = + window.wpApiNotifications?.current_user_id || current_user_id; + + if (currentUserId) { + // Return follow/unfollow structure + return { + follow: { + values: [{ value: String(currentUserId), delete: !toggleValue }], + }, + unfollow: { + values: [{ value: String(currentUserId), delete: toggleValue }], + }, + }; + } + } + return null; + } + // Special case: user_select (uses typeahead, not web component) if (fieldType === 'user_select') { const fieldId = `bulk_${fieldKey}`; @@ -4418,7 +4776,20 @@ ); } - if (share && share['users']) { + // Handle new share payload format: { user_id: , action: 'add' } + if (share && share.user_id && share.action === 'add') { + promises.push( + window.API.add_shared( + list_settings.post_type, + item, + share.user_id, + ).catch((err) => { + console.error(err); + }), + ); + } + // Legacy share format support (backward compatibility) + else if (share && share['users']) { share['users'].forEach(function (value) { let promise = share['unshare'] ? window.API.remove_shared( From 4cc97f5ed5f06ee4180343de673fd0f0375e932f Mon Sep 17 00:00:00 2001 From: corsac Date: Mon, 2 Feb 2026 14:12:01 -0600 Subject: [PATCH 02/10] consistency with comment. --- dt-assets/js/modular-list.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dt-assets/js/modular-list.js b/dt-assets/js/modular-list.js index 7056c6b82c..055e5b5c2b 100644 --- a/dt-assets/js/modular-list.js +++ b/dt-assets/js/modular-list.js @@ -1480,7 +1480,10 @@ renderBulkEditFieldInput(fieldKey, fieldType, inputContainer); // Show clear button for fields that support clearing (exclude comment, follow, and share fields) - if (supportsFieldClearing(fieldType) && fieldType !== 'comment') { + if ( + supportsFieldClearing(fieldType) && + !['comment', 'share', 'follow'].includes(fieldType) + ) { wrapper.find('.bulk-edit-clear-field-btn').show(); } From eb9c19492bb978e07e64e2360ec3387e2a559a3a Mon Sep 17 00:00:00 2001 From: kodinkat Date: Wed, 11 Feb 2026 15:03:33 +0000 Subject: [PATCH 03/10] Enhance bulk edit functionality in modular-list-bulk.js - Introduced support for a new share payload format, allowing for user-specific sharing actions. - Added backward compatibility for legacy share formats to ensure seamless user experience. - Implemented dynamic handling of share and follow fields, improving the bulk edit process. - Refactored share input handling to accommodate both new and legacy formats, enhancing maintainability. - Removed outdated bulk edit field selection logic from modular-list.js, centralizing functionality in modular-list-bulk.js. --- dt-assets/js/modular-list-bulk.js | 385 ++++++- dt-assets/js/modular-list.js | 1635 +---------------------------- 2 files changed, 376 insertions(+), 1644 deletions(-) diff --git a/dt-assets/js/modular-list-bulk.js b/dt-assets/js/modular-list-bulk.js index c601ea35c3..6a179b25b7 100644 --- a/dt-assets/js/modular-list-bulk.js +++ b/dt-assets/js/modular-list-bulk.js @@ -203,7 +203,20 @@ ); } - if (share && share['users']) { + // Handle new share payload format: { user_id: , action: 'add' } + if (share && share.user_id && share.action === 'add') { + promises.push( + window.API.add_shared( + list_settings.post_type, + item, + share.user_id, + ).catch((err) => { + console.error(err); + }), + ); + } + // Legacy share format support (backward compatibility) + else if (share && share['users']) { share['users'].forEach(function (value) { let promise = share['unshare'] ? window.API.remove_shared( @@ -455,7 +468,34 @@ } let updatePayload = {}; - let sharePayload; + let sharePayload = null; + + // Check for share field in dynamically selected fields + const shareFieldSelected = bulkEditSelectedFields?.some( + (f) => f.fieldKey === 'share', + ); + if (shareFieldSelected) { + const shareFieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === 'share', + ); + if (shareFieldData) { + // Collect user ID from input + const shareFieldWrapper = $( + `.bulk-edit-field-wrapper[data-field-key="share"]`, + ); + const shareUserId = collectFieldValue( + 'share', + 'share', + shareFieldWrapper, + ); + if (shareUserId) { + sharePayload = { + user_id: shareUserId, + action: 'add', + }; + } + } + } // Process web component values const form = document.getElementById('bulk_edit_picker'); @@ -572,8 +612,14 @@ const fieldKey = fieldData.fieldKey; const fieldType = fieldData.fieldType; + // Skip share field - it's handled separately via sharePayload + if (fieldType === 'share') { + return; + } + // Skip if field is marked as cleared - set cleared value and ensure it's not overwritten - if (fieldData.cleared) { + // Note: follow field doesn't support clearing (users toggle between states) + if (fieldData.cleared && fieldType !== 'follow') { const clearedValue = getClearedFieldValue(fieldType); // For communication_channel, backend expects: {"contact_email":[],"force_values":true} // We need to create an object that has array-like structure with force_values property @@ -600,8 +646,16 @@ const fieldValue = collectFieldValue(fieldKey, fieldType, fieldWrapper); if (fieldValue !== null && fieldValue !== undefined) { + // Handle follow field specially - add both follow and unfollow to payload + if (fieldType === 'follow') { + // Follow field returns { follow: {...}, unfollow: {...} } structure + if (fieldValue.follow && fieldValue.unfollow) { + updatePayload['follow'] = fieldValue.follow; + updatePayload['unfollow'] = fieldValue.unfollow; + } + } // Handle communication_channel fields specially (direct array format, not wrapped) - if (fieldType === 'communication_channel') { + else if (fieldType === 'communication_channel') { // Communication channel expects direct array: [{"value":"...","key":"..."}] if (Array.isArray(fieldValue)) { updatePayload[fieldKey] = fieldValue; @@ -642,17 +696,20 @@ }); } + // Legacy share input handling (for backward compatibility) shareInput.each(function () { - sharePayload = $(this).data('bulk_key_share'); + const legacySharePayload = $(this).data('bulk_key_share'); + // Only use legacy sharePayload if new sharePayload is not set + if (!sharePayload && legacySharePayload) { + sharePayload = { + users: legacySharePayload, + unshare: $('#bulk_share_unshare').length + ? $('#bulk_share_unshare').prop('checked') + : false, + }; + } }); - let shares = { - users: sharePayload, - unshare: $('#bulk_share_unshare').length - ? $('#bulk_share_unshare').prop('checked') - : false, - }; - let queue = []; let count = 0; $('.bulk_edit_checkbox input').each(function () { @@ -670,7 +727,7 @@ do_each, do_done, updatePayload, - shares, + sharePayload, commentPayload, ); } else { @@ -704,6 +761,100 @@ return null; } + // Special case: share field (not a real post field type) + if (fieldType === 'share') { + const shareInputId = `bulk_share_${fieldKey}`; + const shareInput = fieldWrapper.find(`.js-typeahead-${shareInputId}`); + + if (shareInput.length > 0) { + // Get selected user ID from input data + let selectedUserId = shareInput.data('selected-user-id'); + + // Fallback: check typeahead instance + if (!selectedUserId) { + const typeaheadSelector = `.js-typeahead-${shareInputId}`; + const typeaheadInstance = window.Typeahead?.[typeaheadSelector]; + if ( + typeaheadInstance && + typeaheadInstance.items && + typeaheadInstance.items.length > 0 + ) { + selectedUserId = typeaheadInstance.items[0].ID; + } + } + + // Also store in fieldData for later use + const fieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + if (fieldData && selectedUserId) { + fieldData.shareUserId = selectedUserId; + } + + return selectedUserId || null; + } + return null; + } + + // Special case: follow field (not a real post field type) + if (fieldType === 'follow') { + const followToggleId = `bulk_follow_${fieldKey}`; + const toggleComponent = + fieldWrapper.find(`#${followToggleId}`)[0] || + document.getElementById(followToggleId); + + if (toggleComponent) { + // Get toggle value (true/false) + // dt-toggle component may expose 'value' property (for ComponentService compatibility) + // or 'checked' property, or we need to check internal input element + let toggleValue = false; + + // First, try to get value property (ComponentService expects this) + if ( + toggleComponent.value !== undefined && + toggleComponent.value !== null + ) { + if (typeof toggleComponent.value === 'boolean') { + toggleValue = toggleComponent.value === true; + } else if (typeof toggleComponent.value === 'string') { + toggleValue = toggleComponent.value.toLowerCase() === 'true'; + } else { + toggleValue = Boolean(toggleComponent.value); + } + } + // Fallback to checked property + else if (toggleComponent.checked !== undefined) { + toggleValue = toggleComponent.checked === true; + } + // Last resort: check internal input element + else { + const internalInput = toggleComponent.querySelector( + 'input[type="checkbox"]', + ); + if (internalInput) { + toggleValue = internalInput.checked === true; + } + } + + // Get current user ID + const currentUserId = + window.wpApiNotifications?.current_user_id || current_user_id; + + if (currentUserId) { + // Return follow/unfollow structure + return { + follow: { + values: [{ value: String(currentUserId), delete: !toggleValue }], + }, + unfollow: { + values: [{ value: String(currentUserId), delete: toggleValue }], + }, + }; + } + } + return null; + } + // Special case: user_select (uses typeahead, not web component) if (fieldType === 'user_select') { const fieldId = `bulk_${fieldKey}`; @@ -1020,6 +1171,22 @@ icon: null, }); + // Add synthetic "Share" option (not a real post field) + transformedFields.push({ + id: 'share', + label: window.wpApiShare?.translations?.share || 'Share', + color: null, + icon: null, + }); + + // Add synthetic "Follow" option (not a real post field) + transformedFields.push({ + id: 'follow', + label: window.wpApiShare?.translations?.follow || 'Follow', + color: null, + icon: null, + }); + return transformedFields; } @@ -1234,6 +1401,43 @@ $(), // No icon for comments ); } + } else if (fieldKey === 'share') { + // Handle special 'share' field (not a real post field) + if (!bulkEditSelectedFields.some((f) => f.fieldKey === fieldKey)) { + bulkEditSelectedFields.push({ + fieldKey: fieldKey, + fieldType: 'share', + fieldName: window.wpApiShare?.translations?.share || 'Share', + cleared: false, + shareUserId: null, // Store selected user ID for clear/restore + }); + + // Render the share field + renderBulkEditField( + fieldKey, + 'share', + window.wpApiShare?.translations?.share || 'Share', + $(), // No icon for share + ); + } + } else if (fieldKey === 'follow') { + // Handle special 'follow' field (not a real post field) + if (!bulkEditSelectedFields.some((f) => f.fieldKey === fieldKey)) { + bulkEditSelectedFields.push({ + fieldKey: fieldKey, + fieldType: 'follow', + fieldName: window.wpApiShare?.translations?.follow || 'Follow', + cleared: false, + }); + + // Render the follow field + renderBulkEditField( + fieldKey, + 'follow', + window.wpApiShare?.translations?.follow || 'Follow', + $(), // No icon for follow + ); + } } else { // Handle regular post fields const fieldData = window.post_type_fields[fieldKey]; @@ -1340,8 +1544,11 @@ const inputContainer = wrapper.find('.bulk-edit-field-input-container'); renderBulkEditFieldInput(fieldKey, fieldType, inputContainer); - // Show clear button for fields that support clearing (exclude comment fields) - if (supportsFieldClearing(fieldType) && fieldType !== 'comment') { + // Show clear button for fields that support clearing (exclude comment, follow, and share fields) + if ( + supportsFieldClearing(fieldType) && + !['comment', 'share', 'follow'].includes(fieldType) + ) { wrapper.find('.bulk-edit-clear-field-btn').show(); } @@ -1508,6 +1715,154 @@ return; } + // Handle share field specially + if (fieldType === 'share') { + const shareInputId = `bulk_share_${fieldKey}`; + const shareResultContainerId = `bulk_share-result-container_${fieldKey}`; + + // Build share HTML with typeahead input + let shareHtml = '
'; + shareHtml += + ''; + shareHtml += + '
'; + shareHtml += '
'; + shareHtml += '
'; + shareHtml += ''; + shareHtml += + ''; + shareHtml += ''; + shareHtml += '
'; + shareHtml += '
'; + shareHtml += '
'; + shareHtml += '
'; + + container.html(shareHtml); + + // Initialize typeahead for share field + requestAnimationFrame(() => { + const shareInput = $(`.js-typeahead-${shareInputId}`); + if (shareInput.length === 0) { + return; + } + + // Get field data to check if we need to restore a previous user selection + const fieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + const previousUserId = fieldData?.shareUserId || null; + + // Initialize typeahead (adapted from shared-functions.js share typeahead) + const typeaheadSelector = `.js-typeahead-${shareInputId}`; + window.Typeahead[typeaheadSelector] = jQuery.typeahead({ + input: typeaheadSelector, + minLength: 0, + maxItem: 0, + accent: true, + searchOnFocus: true, + display: ['name'], + template: + '
' + + '' + + '' + + '' + + '
' + + '{{name}}' + + '{{subtitle}}' + + '
' + + '
', + emptyTemplate: window.SHAREDFUNCTIONS.escapeHTML( + window.wpApiShare?.translations?.no_records_found || + 'No records found', + ), + source: window.TYPEAHEADS.typeaheadUserSource(), + displayField: 'name', + matcher: window.TYPEAHEADS.typeaheadMatcher, + callback: { + onClick: function (node, a, item, event) { + // Get fresh reference to fieldData from the array (closure might have stale reference) + const currentFieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + + // Store selected user ID in fieldData + if (currentFieldData) { + currentFieldData.shareUserId = item.ID; + } + // Store in input data for easy retrieval + shareInput.data('selected-user-id', item.ID); + shareInput.data('selected-user-name', item.name); + // Add visual feedback + shareInput.attr('data-selected', 'true'); + // Update input value to show selected user + shareInput.val(item.name); + + // Prevent default and hide layout + event.preventDefault(); + this.hideLayout(); + }, + onResult: function (node, query, result, resultCount) { + if (query === '') { + $(`#${shareResultContainerId}`).html(''); + } else { + let text = window.TYPEAHEADS.typeaheadHelpText( + resultCount, + query, + result, + ); + $(`#${shareResultContainerId}`).html(text); + } + }, + onHideLayout: function () { + $(`#${shareResultContainerId}`).html(''); + }, + }, + }); + + // Restore previous selection if available + if (previousUserId) { + shareInput.data('selected-user-id', previousUserId); + // Note: We don't know the previous user name here, so we leave the input value as-is + shareInput.attr('data-selected', 'true'); + } + }); + + return; + } + + // Handle follow field specially + if (fieldType === 'follow') { + const followToggleId = `bulk_follow_${fieldKey}`; + + // Build follow HTML with dt-toggle component + let followHtml = '
'; + followHtml += + ''; + followHtml += '
'; + + container.html(followHtml); + + return; + } + // Get field settings from list_settings const fieldSettings = window.lodash.get( list_settings, diff --git a/dt-assets/js/modular-list.js b/dt-assets/js/modular-list.js index 8e236fb307..acce8d41f2 100644 --- a/dt-assets/js/modular-list.js +++ b/dt-assets/js/modular-list.js @@ -1099,994 +1099,10 @@ // ============================================ // Bulk Edit Field Selection with dt-multi-select + // NOTE: This functionality has been moved to modular-list-bulk.js + // The bulk module registers itself via DT_List.bulk and handles all bulk edit field selection // ============================================ - let bulkEditSelectedFields = []; - - // Transform field definitions to dt-multi-select format - // Note: We include ALL available fields (including selected ones) in options - // The component's built-in _filterOptions() will automatically filter out selected items from the dropdown - function transformFieldsForMultiSelect(fields) { - const allowedTypes = [ - 'user_select', - 'multi_select', - 'key_select', - 'date', - 'datetime', - 'location_meta', - 'tags', - 'text', - 'textarea', - 'number', - 'connection', // For share, coaches, etc. - 'boolean', // For requires_update, etc. - 'communication_channel', // Communication channel fields - // Note: 'link' fields are hidden until dt-link web component is ready - // Note: 'array' type is not supported as it's typically used for internal data structures - ]; - - const transformedFields = fields - .filter((field) => { - return ( - !field.hidden && // Exclude hidden fields - !field.private && // Exclude private fields - allowedTypes.includes(field.field_type) - ); - }) - .sort((a, b) => { - const nameA = (a.field_name || '').toLowerCase(); - const nameB = (b.field_name || '').toLowerCase(); - return nameA.localeCompare(nameB); - }) - .map((field) => ({ - id: field.field_key, - label: field.field_name, - color: null, - icon: field.icon || null, - })); - - // Add synthetic "Comments" option (not a real post field) - transformedFields.push({ - id: 'comments', - label: window.wpApiShare?.translations?.comments || 'Comments', - color: null, - icon: null, - }); - - // Add synthetic "Share" option (not a real post field) - transformedFields.push({ - id: 'share', - label: window.wpApiShare?.translations?.share || 'Share', - color: null, - icon: null, - }); - - // Add synthetic "Follow" option (not a real post field) - transformedFields.push({ - id: 'follow', - label: window.wpApiShare?.translations?.follow || 'Follow', - color: null, - icon: null, - }); - - return transformedFields; - } - - // Get all available fields - function getAllAvailableFields() { - if (!window.post_type_fields) { - return []; - } - - return Object.entries(window.post_type_fields) - .map(([key, value]) => ({ - field_key: key, - field_name: value.name, - field_type: value.type, - hidden: value.hidden || false, - private: value.private || false, // Include private flag - icon: value.icon || null, - })) - .filter((field) => { - // Exclude hidden and private fields - return !field.hidden && !field.private; - }); - } - - // Update dt-multi-select options - // Note: Include ALL available fields - component will auto-filter selected ones from dropdown - function updateFieldSelectorOptions() { - const fieldSelector = document.querySelector( - 'dt-multi-select[name="bulk_edit_field_selector"]', - ); - if (!fieldSelector) { - return; - } - - const allFields = getAllAvailableFields(); - const allOptions = transformFieldsForMultiSelect(allFields); - - // Set all options (component will filter out selected ones automatically) - fieldSelector.options = allOptions; - } - - // Helper function to create icon element from field data - function createFieldIconElement(fieldData) { - if (!fieldData) { - return $(); - } - - // Get icon from fieldData - check both icon and font-icon properties - const icon = fieldData.icon || fieldData['font-icon']; - - // Validate icon exists and is a non-empty string - if ( - !icon || - typeof icon !== 'string' || - icon.trim() === '' || - icon === 'undefined' - ) { - return $(); - } - - // Create icon element based on type - let iconHtml; - const iconLower = icon.trim().toLowerCase(); - if (iconLower.startsWith('mdi')) { - // Font icon (Material Design Icons) - iconHtml = ``; - } else { - // Image icon - iconHtml = ``; - } - - return $(iconHtml); - } - - // Global dt:get-data event listener for web component data fetching - // Set up once at page load to handle all component data requests - document.addEventListener('dt:get-data', async (e) => { - const { field, query, onSuccess, postType } = e.detail; - const postTypeToUse = postType || list_settings.post_type; - - try { - // For tags and multi-select fields, use multi-select-values endpoint - // For other fields, use field-options endpoint - const fieldSettings = window.lodash.get( - list_settings, - `post_type_settings.fields.${field}`, - {}, - ); - const fieldType = fieldSettings.type || ''; - - let endpoint = 'field-options'; - if (fieldType === 'tags' || fieldType === 'multi_select') { - endpoint = 'multi-select-values'; - } - - const response = await fetch( - `${window.wpApiShare.root}dt-posts/v2/${postTypeToUse}/${endpoint}?field=${field}&s=${encodeURIComponent(query || '')}`, - { - headers: { - 'X-WP-Nonce': window.wpApiShare.nonce, - }, - }, - ); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - if (onSuccess) { - // Handle different response formats - if (data.options) { - onSuccess(data.options); - } else if (Array.isArray(data)) { - // For tags/multi-select, transform to component format - if (fieldType === 'tags') { - const fieldOptions = window.lodash.get( - list_settings, - `post_type_settings.fields.${field}.default`, - {}, - ); - const options = data.map((tag) => { - const label = window.lodash.get( - fieldOptions, - tag + '.label', - tag, - ); - return { id: tag, label: label || tag }; - }); - onSuccess(options); - } else { - onSuccess(data); - } - } else { - onSuccess([]); - } - } - } catch (error) { - if (e.detail.onError) { - e.detail.onError(error); - } - } - }); - - // Initialize dt-multi-select on page load - function initializeBulkEditFieldSelector() { - const fieldSelector = document.querySelector( - 'dt-multi-select[name="bulk_edit_field_selector"]', - ); - if (!fieldSelector) { - return; - } - - // Set initial options - updateFieldSelectorOptions(); - fieldSelector.value = []; - - // Handle change events - fieldSelector.addEventListener('change', function (e) { - // Extract field keys from newValue (could be strings or objects with id property) - const selectedFieldKeys = (e.detail.newValue || []).map((v) => - typeof v === 'string' ? v : v.id || v, - ); - // Extract field keys from oldValue - const previousFieldKeys = (e.detail.oldValue || []).map((v) => - typeof v === 'string' ? v : v.id || v, - ); - - // Find newly selected fields - const newlySelected = selectedFieldKeys.filter( - (key) => !previousFieldKeys.includes(key), - ); - - // Find deselected fields - const deselected = previousFieldKeys.filter( - (key) => !selectedFieldKeys.includes(key), - ); - - // Add newly selected fields - newlySelected.forEach((fieldKey) => { - // Handle special 'comments' field (not a real post field) - if (fieldKey === 'comments') { - if (!bulkEditSelectedFields.some((f) => f.fieldKey === fieldKey)) { - bulkEditSelectedFields.push({ - fieldKey: fieldKey, - fieldType: 'comment', - fieldName: - window.wpApiShare?.translations?.comments || 'Comments', - cleared: false, - }); - - // Render the comment field - renderBulkEditField( - fieldKey, - 'comment', - window.wpApiShare?.translations?.comments || 'Comments', - $(), // No icon for comments - ); - } - } else if (fieldKey === 'share') { - // Handle special 'share' field (not a real post field) - if (!bulkEditSelectedFields.some((f) => f.fieldKey === fieldKey)) { - bulkEditSelectedFields.push({ - fieldKey: fieldKey, - fieldType: 'share', - fieldName: window.wpApiShare?.translations?.share || 'Share', - cleared: false, - shareUserId: null, // Store selected user ID for clear/restore - }); - - // Render the share field - renderBulkEditField( - fieldKey, - 'share', - window.wpApiShare?.translations?.share || 'Share', - $(), // No icon for share - ); - } - } else if (fieldKey === 'follow') { - // Handle special 'follow' field (not a real post field) - if (!bulkEditSelectedFields.some((f) => f.fieldKey === fieldKey)) { - bulkEditSelectedFields.push({ - fieldKey: fieldKey, - fieldType: 'follow', - fieldName: window.wpApiShare?.translations?.follow || 'Follow', - cleared: false, - }); - - // Render the follow field - renderBulkEditField( - fieldKey, - 'follow', - window.wpApiShare?.translations?.follow || 'Follow', - $(), // No icon for follow - ); - } - } else { - // Handle regular post fields - const fieldData = window.post_type_fields[fieldKey]; - if ( - fieldData && - !bulkEditSelectedFields.some((f) => f.fieldKey === fieldKey) - ) { - bulkEditSelectedFields.push({ - fieldKey: fieldKey, - fieldType: fieldData.type, - fieldName: fieldData.name, - cleared: false, - }); - - // Create icon element from field data - const iconElement = createFieldIconElement(fieldData); - - // Render the field - renderBulkEditField( - fieldKey, - fieldData.type, - fieldData.name, - iconElement, - ); - } - } - }); - - // Remove deselected fields - deselected.forEach((fieldKey) => { - $(`.bulk-edit-field-wrapper[data-field-key="${fieldKey}"]`).remove(); - bulkEditSelectedFields = bulkEditSelectedFields.filter( - (f) => f.fieldKey !== fieldKey, - ); - }); - - // Update hidden input and button state - updateBulkEditSelectedFieldsInput(); - updateBulkEditButtonState(); - - // Ensure dropdown closes after selection - // Use requestAnimationFrame to ensure the component has processed the change - requestAnimationFrame(() => { - if (fieldSelector && fieldSelector.open) { - fieldSelector.open = false; - } - }); - - // Note: No need to update options - component auto-filters selected items from dropdown - // But we do need to ensure options include all fields so selected ones can be displayed as tags - updateFieldSelectorOptions(); - }); - } - - // Initialize on page load - if ($('dt-multi-select[name="bulk_edit_field_selector"]').length) { - // Wait for web components to be ready - if (window.customElements && window.customElements.get('dt-multi-select')) { - initializeBulkEditFieldSelector(); - } else { - // Wait for component to be defined - $(document).ready(function () { - setTimeout(initializeBulkEditFieldSelector, 100); - }); - } - } - - function renderBulkEditField( - fieldKey, - fieldType, - fieldName, - fieldIconElement, - ) { - const container = $('#bulk_edit_selected_fields_container'); - const template = $('#bulk_edit_field_template').html(); - const wrapper = $(template); - - // Set field key - wrapper.attr('data-field-key', fieldKey); - wrapper.find('.bulk-edit-field-name').text(fieldName); - wrapper - .find('.bulk-edit-remove-field-btn') - .attr('data-field-key', fieldKey); - wrapper.find('.bulk-edit-clear-field-btn').attr('data-field-key', fieldKey); - wrapper - .find('.bulk-edit-restore-field-btn') - .attr('data-field-key', fieldKey); - - // Set icon - const iconContainer = wrapper.find('.bulk-edit-field-icon'); - if (fieldIconElement && fieldIconElement.length) { - iconContainer.html(fieldIconElement.clone()); - } else { - iconContainer.html( - '
', - ); - } - - // Render field input based on type - const inputContainer = wrapper.find('.bulk-edit-field-input-container'); - renderBulkEditFieldInput(fieldKey, fieldType, inputContainer); - - // Show clear button for fields that support clearing (exclude comment, follow, and share fields) - if ( - supportsFieldClearing(fieldType) && - !['comment', 'share', 'follow'].includes(fieldType) - ) { - wrapper.find('.bulk-edit-clear-field-btn').show(); - } - - // Append to container - container.append(wrapper); - } - - function renderBulkEditFieldInput(fieldKey, fieldType, container) { - // Handle comment field specially - if (fieldType === 'comment') { - // Create unique IDs for this comment field instance - const commentInputId = `bulk_comment-input_${fieldKey}`; - const commentTypeSelectorId = `comment_type_selector_${fieldKey}`; - - // Build comment HTML with proper spacing - let commentHtml = '
'; - commentHtml += - ''; - - // Add Type selector with proper spacing - commentHtml += '
'; - commentHtml += - '
' + - (window.wpApiShare?.translations?.type || 'Type:') + - '
'; - commentHtml += - ''; - commentHtml += '
'; - commentHtml += '
'; - - container.html(commentHtml); - - // Populate comment type selector options from hidden data element - // Use requestAnimationFrame to ensure DOM is ready - requestAnimationFrame(() => { - const newSelector = $(`#${commentTypeSelectorId}`); - - if (newSelector.length === 0) { - return; - } - - // Get comment sections from hidden JSON data element - const commentSectionsData = document.getElementById( - 'bulk_edit_comment_sections_data', - ); - if (commentSectionsData) { - try { - // Get text content from the script element - const jsonText = - commentSectionsData.textContent || commentSectionsData.innerText; - if (!jsonText || jsonText.trim() === '') { - // Fallback: ensure default 'comment' option exists - if (newSelector.find('option').length === 0) { - newSelector.append( - '', - ); - newSelector.val('comment'); - } - return; - } - - const commentSectionsRaw = JSON.parse(jsonText.trim()); - - // Convert to array if it's an object with numeric keys - let commentSections = []; - if (Array.isArray(commentSectionsRaw)) { - commentSections = commentSectionsRaw; - } else if ( - typeof commentSectionsRaw === 'object' && - commentSectionsRaw !== null - ) { - // Convert object to array - commentSections = Object.values(commentSectionsRaw); - } - - if (commentSections.length > 0) { - // Clear default option and add all sections - newSelector.empty(); - - // Add all comment sections - commentSections.forEach((section) => { - if (section && section.key) { - // Skip 'activity' as it's not a comment type - if (section.key !== 'activity') { - // Get label - check multiple possible properties - const label = section.label || section.name || section.key; - const enabled = section.enabled !== false; // Default to enabled if not specified - - // Only add if enabled (unless it's the default comment type) - if (enabled || section.key === 'comment') { - newSelector.append( - ``, - ); - } - } - } - }); - - // Ensure at least one option exists (fallback to 'comment' if empty) - if (newSelector.find('option').length === 0) { - newSelector.append( - '', - ); - } - - // Set default value to 'comment' if available, otherwise first option - if (newSelector.find('option[value="comment"]').length > 0) { - newSelector.val('comment'); - } else if (newSelector.find('option').length > 0) { - newSelector.val( - newSelector.find('option').first().attr('value'), - ); - } - } else { - // Fallback: ensure default 'comment' option exists - if (newSelector.find('option').length === 0) { - newSelector.append( - '', - ); - newSelector.val('comment'); - } - } - } catch (e) { - // Fallback: ensure default 'comment' option exists - if (newSelector.find('option').length === 0) { - newSelector.append( - '', - ); - newSelector.val('comment'); - } - } - } else { - // Fallback: ensure default 'comment' option exists - if (newSelector.find('option').length === 0) { - newSelector.append( - '', - ); - newSelector.val('comment'); - } - } - }); - - return; - } - - // Handle share field specially - if (fieldType === 'share') { - const shareInputId = `bulk_share_${fieldKey}`; - const shareResultContainerId = `bulk_share-result-container_${fieldKey}`; - - // Build share HTML with typeahead input - let shareHtml = '
'; - shareHtml += - ''; - shareHtml += - '
'; - shareHtml += '
'; - shareHtml += '
'; - shareHtml += ''; - shareHtml += - ''; - shareHtml += ''; - shareHtml += '
'; - shareHtml += '
'; - shareHtml += '
'; - shareHtml += '
'; - - container.html(shareHtml); - - // Initialize typeahead for share field - requestAnimationFrame(() => { - const shareInput = $(`.js-typeahead-${shareInputId}`); - if (shareInput.length === 0) { - return; - } - - // Get field data to check if we need to restore a previous user selection - const fieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - const previousUserId = fieldData?.shareUserId || null; - - // Initialize typeahead (adapted from shared-functions.js share typeahead) - const typeaheadSelector = `.js-typeahead-${shareInputId}`; - window.Typeahead[typeaheadSelector] = jQuery.typeahead({ - input: typeaheadSelector, - minLength: 0, - maxItem: 0, - accent: true, - searchOnFocus: true, - template: function (query, item) { - return `
-
- - {{name}} (#${window.SHAREDFUNCTIONS.escapeHTML(item.ID)}) -
-
`; - }, - source: window.TYPEAHEADS.typeaheadUserSource(), - emptyTemplate: window.SHAREDFUNCTIONS.escapeHTML( - window.wpApiShare?.translations?.no_records_found || - 'No records found', - ), - display: 'name', - templateValue: '{{name}}', - dynamic: true, - callback: { - onClick: function (node, a, item, event) { - // Get fresh reference to fieldData from the array (closure might have stale reference) - const currentFieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - - // Store selected user ID in fieldData - if (currentFieldData) { - currentFieldData.shareUserId = item.ID; - } - // Store in input data for easy retrieval - shareInput.data('selected-user-id', item.ID); - shareInput.data('selected-user-name', item.name); - // Add visual feedback - shareInput.attr('data-selected', 'true'); - // Update input value to show selected user - shareInput.val(item.name); - - // Prevent default and hide layout - event.preventDefault(); - this.hideLayout(); - }, - onResult: function (node, query, result, resultCount) { - if (query) { - let text = window.TYPEAHEADS.typeaheadHelpText( - resultCount, - query, - result, - ); - $(`#${shareResultContainerId}`).html(text); - } - }, - onHideLayout: function () { - $(`#${shareResultContainerId}`).html(''); - }, - }, - }); - - // If we have a previous user ID (from restore), try to pre-select it - if (previousUserId) { - // Note: This would require fetching user data, which might be complex - // For now, we'll just restore the user_id in fieldData - // The user will need to re-select if they want to see it visually - if (fieldData) { - fieldData.shareUserId = previousUserId; - } - shareInput.data('selected-user-id', previousUserId); - } - }); - - return; - } - - // Handle follow field specially - if (fieldType === 'follow') { - const followToggleId = `bulk_follow_${fieldKey}`; - - // Build follow HTML with dt-toggle component - let followHtml = '
'; - followHtml += - ''; - followHtml += '
'; - - container.html(followHtml); - - // Initialize toggle value (default to false - not following) - requestAnimationFrame(() => { - const toggleComponent = document.getElementById(followToggleId); - if (toggleComponent) { - // Set default value to false (not following) - // dt-toggle component uses 'checked' property - if (toggleComponent.checked !== undefined) { - toggleComponent.checked = false; - } else if (toggleComponent.value !== undefined) { - // Fallback: some implementations might use value property - toggleComponent.value = false; - } else { - // Set internal input element if component doesn't expose checked directly - const internalInput = toggleComponent.querySelector( - 'input[type="checkbox"]', - ); - if (internalInput) { - internalInput.checked = false; - } - } - // Optional: Check current user's follow state if available - // For now, default to false - } - }); - - return; - } - - // Get field settings from list_settings - const fieldSettings = window.lodash.get( - list_settings, - `post_type_settings.fields.${fieldKey}`, - null, - ); - - if (!fieldSettings) { - container.html( - '
Error: Field settings not found
', - ); - return; - } - - // Generate field HTML client-side using helper function - const fieldHtml = window.SHAREDFUNCTIONS.renderField( - fieldKey, - fieldSettings, - 'bulk_', - ); - - if (!fieldHtml) { - // Field type not supported - container.html( - '
This field type is not yet supported in bulk edit
', - ); - return; - } - - // Inject HTML directly into container - container.html(fieldHtml); - - // Initialize web components after DOM injection - requestAnimationFrame(() => { - // Initialize ComponentService to set up all web components - if (window.componentService && window.componentService.initialize) { - try { - window.componentService.initialize(); - } catch (e) { - // ComponentService initialization error - components should still work - } - } - - // Initialize field-specific handlers if needed - initializeBulkEditFieldHandlers(fieldKey, fieldType); - }); - } - - function initializeBulkEditFieldHandlers(fieldKey, fieldType) { - // Special case: user_select uses typeahead (not a web component) - if (fieldType === 'user_select') { - const fieldId = `bulk_${fieldKey}`; - const userInput = $(`.js-typeahead-${fieldId}`); - - if (userInput.length && !window.Typeahead[`.js-typeahead-${fieldId}`]) { - $.typeahead({ - input: `.js-typeahead-${fieldId}`, - minLength: 0, - maxItem: 0, - accent: true, - searchOnFocus: true, - source: window.TYPEAHEADS.typeaheadUserSource(), - templateValue: '{{name}}', - template: function (query, item) { - return `
- - - ${window.SHAREDFUNCTIONS.escapeHTML(item.name)} - - ${item.status_color ? ` ` : ''} - ${ - item.update_needed && item.update_needed > 0 - ? ` - - ${window.SHAREDFUNCTIONS.escapeHTML(item.update_needed)} - ` - : '' - } -
`; - }, - dynamic: true, - hint: true, - emptyTemplate: window.SHAREDFUNCTIONS.escapeHTML( - window.wpApiShare.translations.no_records_found, - ), - callback: { - onClick: function (node, a, item, event) { - event.preventDefault(); - this.hideLayout(); - this.resetInput(); - - // Set the selected user value - const resultContainer = $(`#${fieldId}-result-container`); - resultContainer.html( - `${window.SHAREDFUNCTIONS.escapeHTML(item.name)}`, - ); - - // Store the selected user ID in data attributes for later collection - userInput.data('selected-user-id', item.ID); - userInput.data('selected-user-name', item.name); - resultContainer.data('selected-user-id', item.ID); - resultContainer.data('selected-user-name', item.name); - }, - onResult: function (node, query, result, resultCount) { - const resultContainer = $(`#${fieldId}-result-container`); - if (resultCount > 0) { - resultContainer.html( - `${resultCount} ${window.wpApiShare.translations.user_found || 'user(s) found'}`, - ); - } else { - resultContainer.html(''); - } - }, - onHideLayout: function () { - $(`#${fieldId}-result-container`).html(''); - }, - }, - }); - } - return; - } - - // For all web components: ComponentService.initialize() handles initialization - // The global dt:get-data listener handles data fetching - // No per-field-type initialization needed - } - - // Remove field when clicking X button - $(document).on('click', '.bulk-edit-remove-field-btn', function () { - const fieldKey = $(this).data('field-key'); - - // Remove from selected fields array - bulkEditSelectedFields = bulkEditSelectedFields.filter( - (f) => f.fieldKey !== fieldKey, - ); - - // Remove field wrapper from DOM - $(`.bulk-edit-field-wrapper[data-field-key="${fieldKey}"]`).remove(); - - // Update hidden input - updateBulkEditSelectedFieldsInput(); - - // Update update button state based on field selection - updateBulkEditButtonState(); - - // Update dt-multi-select to remove the field from selection - const fieldSelector = document.querySelector( - 'dt-multi-select[name="bulk_edit_field_selector"]', - ); - if (fieldSelector) { - const currentValue = fieldSelector.value || []; - fieldSelector.value = currentValue.filter((v) => { - const id = typeof v === 'string' ? v : v.id; - return id !== fieldKey; - }); - // Update options to show the field again - updateFieldSelectorOptions(); - } - }); - - function updateBulkEditSelectedFieldsInput() { - const fieldKeys = bulkEditSelectedFields.map((f) => f.fieldKey); - $('#bulk_edit_selected_fields_input').val(JSON.stringify(fieldKeys)); - } - - function supportsFieldClearing(fieldType) { - // Fields that support clearing/unsetting - const clearableTypes = [ - 'connection', - 'multi_select', - 'tags', - 'user_select', - 'date', - 'datetime', - 'location', - 'location_meta', - 'text', - 'textarea', - 'number', - 'key_select', - 'communication_channel', - ]; - return clearableTypes.includes(fieldType); - } - - // Clear/unset field value - $(document).on('click', '.bulk-edit-clear-field-btn', function () { - const fieldKey = $(this).data('field-key'); - const fieldWrapper = $( - `.bulk-edit-field-wrapper[data-field-key="${fieldKey}"]`, - ); - const fieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - - if (!fieldData) return; - - // Mark field as cleared - fieldData.cleared = true; - - // Clear the input visually - const inputContainer = fieldWrapper.find( - '.bulk-edit-field-input-container', - ); - inputContainer.html( - '
Field will be cleared/unset
', - ); - - // Hide clear button, show restore button - $(this).hide(); - fieldWrapper.find('.bulk-edit-restore-field-btn').show(); - }); - - // Restore field value (undo clear) - $(document).on('click', '.bulk-edit-restore-field-btn', function () { - const fieldKey = $(this).data('field-key'); - const fieldWrapper = $( - `.bulk-edit-field-wrapper[data-field-key="${fieldKey}"]`, - ); - const fieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - - if (!fieldData) return; - - // Remove cleared flag - fieldData.cleared = false; - - // Re-render field input - const inputContainer = fieldWrapper.find( - '.bulk-edit-field-input-container', - ); - renderBulkEditFieldInput(fieldKey, fieldData.fieldType, inputContainer); - - // Show clear button, hide restore button - $(this).hide(); - fieldWrapper.find('.bulk-edit-clear-field-btn').show(); - - // Update update button state (field is no longer cleared) - updateBulkEditButtonState(); - }); - archivedSwitch.on('click', function () { const showArchived = this.checked; @@ -4048,31 +3064,11 @@ /*** * Bulk Edit + * NOTE: Bulk edit functionality has been moved to modular-list-bulk.js + * The bulk module handles all bulk edit operations including checkbox events, + * field selection, and submit handling */ - $('#bulk_edit_controls').on('click', function () { - $('#bulk_edit_picker').toggle(); - $('#records-table').toggleClass('bulk_edit_on'); - }); - - // Old bulk_edit_seeMore handler removed - replaced with dynamic field selection - - function bulk_edit_checkbox_event() { - $('tbody tr td.bulk_edit_checkbox').on('click', function (e) { - e.stopImmediatePropagation(); - bulk_edit_count(); - }); - } - - $('#bulk_edit_master').on('click', function (e) { - e.stopImmediatePropagation(); - let checked = $(this).children('input').is(':checked'); - $('.bulk_edit_checkbox input').each(function () { - $(this).prop('checked', checked); - bulk_edit_count(); - }); - }); - /*** * Bulk Delete */ @@ -4121,627 +3117,8 @@ /** * Bulk_Assigned_to + * NOTE: Bulk edit submit functionality has been moved to modular-list-bulk.js */ - let bulk_edit_submit_button = $('#bulk_edit_submit'); - - // Prevent form submission (button handles it via JavaScript) - $('#bulk_edit_picker').on('submit', function (e) { - e.preventDefault(); - e.stopPropagation(); - return false; - }); - - bulk_edit_submit_button.on('click', function (e) { - e.preventDefault(); - e.stopPropagation(); - bulk_edit_submit(); - return false; - }); - - function bulk_edit_submit() { - $('#bulk_edit_submit-spinner').addClass('active'); - let allInputs = $( - '#bulk_edit_picker input, #bulk_edit_picker select, #bulk_edit_picker .button', - ).not('#bulk_share'); - let multiSelectInputs = $('#bulk_edit_picker .dt_multi_select'); - let shareInput = $('#bulk_share'); - let commentPayload = {}; - // Check for comment field in dynamically selected fields - const commentFieldSelected = bulkEditSelectedFields?.some( - (f) => f.fieldKey === 'comments', - ); - if (commentFieldSelected) { - // Find the comment input for the comments field - const commentInput = $( - '#bulk_edit_selected_fields_container textarea[id^="bulk_comment-input_"]', - ); - if (commentInput.length > 0) { - commentPayload['commentText'] = commentInput.val(); - // Get the corresponding comment type selector - const commentTypeSelector = commentInput - .closest('.bulk-edit-field-input-container') - .find('select[id^="comment_type_selector_"]'); - if (commentTypeSelector.length > 0) { - commentPayload['commentType'] = commentTypeSelector.val(); - } else { - // Fallback to default selector if exists - commentPayload['commentType'] = - $('#comment_type_selector').val() || 'comment'; - } - } - } - - let updatePayload = {}; - let sharePayload = null; - - // Check for share field in dynamically selected fields - const shareFieldSelected = bulkEditSelectedFields?.some( - (f) => f.fieldKey === 'share', - ); - if (shareFieldSelected) { - const shareFieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === 'share', - ); - if (shareFieldData) { - // Collect user ID from input - const shareFieldWrapper = $( - `.bulk-edit-field-wrapper[data-field-key="share"]`, - ); - const shareUserId = collectFieldValue( - 'share', - 'share', - shareFieldWrapper, - ); - if (shareUserId) { - sharePayload = { - user_id: shareUserId, - action: 'add', - }; - } - } - } - - // Process web component values - const form = document.getElementById('bulk_edit_picker'); - Array.from(form.querySelectorAll('*')).forEach((el) => { - // skip fields not from web components - if (!el.tagName || !el.tagName.startsWith('DT-')) { - return; - } - - // Skip the field selector component - it's not a field to update - const fieldName = el.name ? el.name.trim() : null; - if ( - fieldName === 'bulk_edit_field_selector' || - el.id === 'bulk_edit_field_selector' - ) { - return; - } - - // Skip any components inside the dynamically selected fields container - // (they will be processed in the dynamically selected fields section) - if (el.closest('#bulk_edit_selected_fields_container')) { - return; - } - - if (el.value) { - const convertedValue = - window.DtWebComponents.ComponentService.convertValue( - el.tagName, - el.value, - ); - - // Only add to payload if value is not empty - // For array-based fields, check if values array has items - if (convertedValue !== null && convertedValue !== undefined) { - if (typeof convertedValue === 'object' && convertedValue.values) { - // For array-based fields, only include if there are values or force_values is true - if ( - convertedValue.values.length > 0 || - convertedValue.force_values === true - ) { - updatePayload[fieldName] = convertedValue; - } - } else { - // For simple fields, include if not empty - if (convertedValue !== '' && convertedValue !== null) { - updatePayload[fieldName] = convertedValue; - } - } - } - } - }); - - // Process legacy components - allInputs.each(function () { - let inputData = $(this).data(); - $.each(inputData, function (key, value) { - if (key.includes('bulk_key_') && value) { - let field_key = key.replace('bulk_key_', ''); - if (field_key) { - updatePayload[field_key] = value; - } - } - }); - }); - if (window.location_data) { - updatePayload['location_grid_meta'] = - window.location_data.location_grid_meta; - } - - let multiSelectUpdatePayload = {}; - multiSelectInputs.each(function () { - // Skip if this input is inside the dynamically selected fields container - if ($(this).closest('#bulk_edit_selected_fields_container').length > 0) { - return; // Skip - will be handled in dynamically selected fields section - } - - let inputData = $(this).data(); - $.each(inputData, function (key, value) { - if (key.includes('bulk_key_') && value) { - let field_key = key.replace('bulk_key_', ''); - // Skip if this field is in dynamically selected fields (regardless of cleared status) - const fieldData = bulkEditSelectedFields?.find( - (f) => f.fieldKey === field_key, - ); - if (fieldData) { - return; // Skip processing - will be handled in dynamically selected fields section - } - if (!multiSelectUpdatePayload[field_key]) { - multiSelectUpdatePayload[field_key] = { values: [] }; - } - multiSelectUpdatePayload[field_key].values.push(value.values); - } - }); - }); - const multiSelectKeys = Object.keys(multiSelectUpdatePayload); - - multiSelectKeys.forEach((key, index) => { - // Double-check: don't overwrite if field is cleared in dynamically selected fields - const fieldData = bulkEditSelectedFields?.find((f) => f.fieldKey === key); - if (!fieldData || !fieldData.cleared) { - // Also check if this field is already in updatePayload (from cleared fields) - if (!updatePayload[key] || !updatePayload[key].force_values) { - updatePayload[key] = multiSelectUpdatePayload[key]; - } - } - }); - - // Process dynamically selected fields - if ( - typeof bulkEditSelectedFields !== 'undefined' && - bulkEditSelectedFields.length > 0 - ) { - bulkEditSelectedFields.forEach(function (fieldData) { - const fieldKey = fieldData.fieldKey; - const fieldType = fieldData.fieldType; - - // Skip share field - it's handled separately via sharePayload - if (fieldType === 'share') { - return; - } - - // Skip if field is marked as cleared - set cleared value and ensure it's not overwritten - // Note: follow field doesn't support clearing (users toggle between states) - if (fieldData.cleared && fieldType !== 'follow') { - const clearedValue = getClearedFieldValue(fieldType); - // For communication_channel, backend expects: {"contact_email":[],"force_values":true} - // We need to create an object that has array-like structure with force_values property - if (fieldType === 'communication_channel') { - // Create an object that will serialize correctly - // Backend checks: is_array($fields[$details_key]) AND $fields[$details_key]['force_values'] - // In JavaScript, we can't have an array with properties that serialize, so we use an object - // But backend also accepts: $fields[$details_key]['values'] format - // So we'll use: {values: [], force_values: true} which backend will handle - updatePayload[fieldKey] = { - values: [], - force_values: true, - }; - } else { - updatePayload[fieldKey] = clearedValue; - } - return; - } - - // Collect value from field input - const fieldWrapper = $( - `.bulk-edit-field-wrapper[data-field-key="${fieldKey}"]`, - ); - const fieldValue = collectFieldValue(fieldKey, fieldType, fieldWrapper); - - if (fieldValue !== null && fieldValue !== undefined) { - // Handle follow field specially - add both follow and unfollow to payload - if (fieldType === 'follow') { - // Follow field returns { follow: {...}, unfollow: {...} } structure - if (fieldValue.follow && fieldValue.unfollow) { - updatePayload['follow'] = fieldValue.follow; - updatePayload['unfollow'] = fieldValue.unfollow; - } - } - // Handle communication_channel fields specially (direct array format, not wrapped) - else if (fieldType === 'communication_channel') { - // Communication channel expects direct array: [{"value":"...","key":"..."}] - if (Array.isArray(fieldValue)) { - updatePayload[fieldKey] = fieldValue; - } - } - // Handle array-based fields specially (multi_select, tags, connection, location_meta) - else if ( - fieldType === 'multi_select' || - fieldType === 'tags' || - fieldType === 'connection' || - fieldType === 'location_meta' - ) { - // Array-based fields return { values: [...] } format - // We need to preserve the structure and ensure force_values is set - if (fieldValue.values && Array.isArray(fieldValue.values)) { - updatePayload[fieldKey] = { - values: fieldValue.values, - force_values: - fieldValue.force_values !== undefined - ? fieldValue.force_values - : false, - }; - } - } else { - // For boolean fields, include even if false (false is a valid value) - if (fieldType === 'boolean') { - updatePayload[fieldKey] = fieldValue === true; - } else { - updatePayload[fieldKey] = fieldValue; - } - } - } else { - // For boolean fields, if no value was collected, default to false - if (fieldType === 'boolean') { - updatePayload[fieldKey] = false; - } - } - }); - } - - // Legacy share input handling (for backward compatibility) - shareInput.each(function () { - const legacySharePayload = $(this).data('bulk_key_share'); - // Only use legacy sharePayload if new sharePayload is not set - if (!sharePayload && legacySharePayload) { - sharePayload = { - users: legacySharePayload, - unshare: $('#bulk_share_unshare').length - ? $('#bulk_share_unshare').prop('checked') - : false, - }; - } - }); - - let queue = []; - let count = 0; - $('.bulk_edit_checkbox input').each(function () { - if (this.checked && this.id !== 'bulk_edit_master_checkbox') { - let postId = parseInt($(this).val()); - queue.push(postId); - } - }); - - // Process the queue to update records - if (queue.length > 0) { - process( - queue, - 10, - do_each, - do_done, - updatePayload, - sharePayload, - commentPayload, - ); - } else { - $('#bulk_edit_submit-spinner').removeClass('active'); - } - } - - function collectFieldValue(fieldKey, fieldType, fieldWrapper) { - // Special case: comment field (not a real post field type) - if (fieldType === 'comment') { - const commentInputId = `bulk_comment-input_${fieldKey}`; - const commentInput = fieldWrapper.find(`#${commentInputId}`); - if (commentInput.length > 0) { - const commentText = commentInput.val(); - return commentText && commentText.trim() !== '' - ? commentText.trim() - : null; - } - // Fallback: check the original comment input - const originalCommentInput = $('#bulk_comment-input'); - if (originalCommentInput.length > 0) { - const commentText = originalCommentInput.val(); - return commentText && commentText.trim() !== '' - ? commentText.trim() - : null; - } - return null; - } - - // Special case: share field (not a real post field type) - if (fieldType === 'share') { - const shareInputId = `bulk_share_${fieldKey}`; - const shareInput = fieldWrapper.find(`.js-typeahead-${shareInputId}`); - - if (shareInput.length > 0) { - // Get selected user ID from input data - let selectedUserId = shareInput.data('selected-user-id'); - - // Fallback: check typeahead instance - if (!selectedUserId) { - const typeaheadSelector = `.js-typeahead-${shareInputId}`; - const typeaheadInstance = window.Typeahead?.[typeaheadSelector]; - if ( - typeaheadInstance && - typeaheadInstance.items && - typeaheadInstance.items.length > 0 - ) { - selectedUserId = typeaheadInstance.items[0].ID; - } - } - - // Also store in fieldData for later use - const fieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - if (fieldData && selectedUserId) { - fieldData.shareUserId = selectedUserId; - } - - return selectedUserId || null; - } - return null; - } - - // Special case: follow field (not a real post field type) - if (fieldType === 'follow') { - const followToggleId = `bulk_follow_${fieldKey}`; - const toggleComponent = - fieldWrapper.find(`#${followToggleId}`)[0] || - document.getElementById(followToggleId); - - if (toggleComponent) { - // Get toggle value (true/false) - // dt-toggle component may expose 'value' property (for ComponentService compatibility) - // or 'checked' property, or we need to check internal input element - let toggleValue = false; - - // First, try to get value property (ComponentService expects this) - if ( - toggleComponent.value !== undefined && - toggleComponent.value !== null - ) { - if (typeof toggleComponent.value === 'boolean') { - toggleValue = toggleComponent.value === true; - } else if (typeof toggleComponent.value === 'string') { - toggleValue = toggleComponent.value.toLowerCase() === 'true'; - } else { - toggleValue = Boolean(toggleComponent.value); - } - } - // Fallback to checked property - else if (toggleComponent.checked !== undefined) { - toggleValue = toggleComponent.checked === true; - } - // Last resort: check internal input element - else { - const internalInput = toggleComponent.querySelector( - 'input[type="checkbox"]', - ); - if (internalInput) { - toggleValue = internalInput.checked === true; - } - } - - // Get current user ID - const currentUserId = - window.wpApiNotifications?.current_user_id || current_user_id; - - if (currentUserId) { - // Return follow/unfollow structure - return { - follow: { - values: [{ value: String(currentUserId), delete: !toggleValue }], - }, - unfollow: { - values: [{ value: String(currentUserId), delete: toggleValue }], - }, - }; - } - } - return null; - } - - // Special case: user_select (uses typeahead, not web component) - if (fieldType === 'user_select') { - const fieldId = `bulk_${fieldKey}`; - const userInput = fieldWrapper.find(`.js-typeahead-${fieldId}`); - if (userInput.length > 0) { - const selectedUserId = userInput.data('selected-user-id'); - if (selectedUserId) { - return `user-${selectedUserId}`; - } - // Fallback: check typeahead instance - const typeaheadSelector = `.js-typeahead-${fieldId}`; - const typeaheadInstance = window.Typeahead?.[typeaheadSelector]; - if ( - typeaheadInstance && - typeaheadInstance.items && - typeaheadInstance.items.length > 0 - ) { - const selectedItem = typeaheadInstance.items[0]; - if (selectedItem && selectedItem.ID) { - return `user-${selectedItem.ID}`; - } - } - } - return null; - } - - // Special case: communication_channel (dt-multi-text needs special formatting) - if (fieldType === 'communication_channel') { - const multiTextComponent = - fieldWrapper.find(`dt-multi-text#bulk_${fieldKey}`)[0] || - fieldWrapper.find(`dt-multi-text[name="${fieldKey}"]`)[0] || - fieldWrapper.find(`dt-multi-text`)[0]; - - if (multiTextComponent && multiTextComponent.value) { - // dt-multi-text stores values as array of objects - // Backend expects direct array format: [{"verified":false,"value":"abc@email.com"}] - if (Array.isArray(multiTextComponent.value)) { - return multiTextComponent.value.map((item) => { - const result = { - value: item.value || '', - }; - if (item.key && item.key !== 'new') { - result.key = item.key; - } - if (item.verified !== undefined) { - result.verified = item.verified; - } - return result; - }); - } - } - return null; - } - - // For all other fields: find web component and use ComponentService - const component = fieldWrapper.find( - 'dt-text, dt-textarea, dt-number, dt-toggle, dt-date, dt-single-select, ' + - 'dt-multi-select, dt-multi-select-button-group, dt-multi-text, dt-tags, ' + - 'dt-connection, dt-location, dt-location-map, dt-user-select', - )[0]; - - if (!component?.value) { - // Fallback for simple input fields (text, textarea, number, date, key_select) - const input = fieldWrapper.find(`#bulk_${fieldKey}`); - if (input.length > 0) { - if ( - fieldType === 'text' || - fieldType === 'textarea' || - fieldType === 'number' - ) { - return input.val() || ''; - } - if (fieldType === 'date' || fieldType === 'datetime') { - return input.val() || null; - } - if (fieldType === 'key_select') { - return input.val() || null; - } - } - return null; - } - - // Use ComponentService to convert component value - return ( - window.DtWebComponents?.ComponentService?.convertValue( - component.tagName, - component.value, - ) ?? component.value - ); - } - - function getClearedFieldValue(fieldType) { - // Return appropriate cleared value based on field type - // Backend requires force_values: true for array-based fields to properly clear all values - switch (fieldType) { - case 'connection': - case 'tags': - case 'location': - case 'multi_select': - case 'location_meta': - // Use force_values: true to clear all values (backend requirement) - // Backend checks for force_values to delete all existing values before processing new ones - return { values: [], force_values: true }; - - case 'communication_channel': - // Communication channel expects direct array format with force_values - // Backend format: {"contact_email":[],"force_values":true} - return []; - - case 'date': - case 'datetime': - case 'text': - case 'textarea': - case 'number': - case 'key_select': - case 'user_select': - // Return empty string to reset field value - return ''; - - default: - return null; - } - } - - function updateBulkEditButtonState() { - const updateButton = $('#bulk_edit_submit'); - const hasSelectedFields = - bulkEditSelectedFields && bulkEditSelectedFields.length > 0; - const hasSelectedRecords = - $('.bulk_edit_checkbox:not(#bulk_edit_master) input:checked').length > 0; - - // Enable update button only if both records and fields are selected - if (hasSelectedRecords && hasSelectedFields) { - updateButton.prop('disabled', false); - } else { - updateButton.prop('disabled', true); - } - } - - function bulk_edit_count() { - let bulk_edit_total_checked = $( - '.bulk_edit_checkbox:not(#bulk_edit_master) input:checked', - ).length; - let bulk_edit_submit_button_text = $('.bulk_edit_submit_text'); - let bulk_edit_delete_submit_button_text = $( - '.bulk_edit_delete_submit_text', - ); - let noSelectionMessage = $('#bulk_edit_no_selection_message'); - let actionButtons = $('#bulk_edit_action_buttons'); - - if (bulk_edit_total_checked == 0) { - // Hide buttons, show instruction message - noSelectionMessage.show(); - actionButtons.hide(); - - bulk_edit_submit_button_text.text( - `${list_settings.translations.make_selections_below}`, - ); - - if (list_settings.permissions.delete_any) { - bulk_edit_delete_submit_button_text.text( - `${list_settings.translations.delete_selections_below}`, - ); - } - } else { - // Show buttons, hide instruction message - noSelectionMessage.hide(); - actionButtons.show(); - - bulk_edit_submit_button_text.each(function (index) { - let pretext = $(this).data('pretext'); - let posttext = $(this).data('posttext'); - $(this).text(`${pretext} ${bulk_edit_total_checked} ${posttext}`); - }); - - if (list_settings.permissions.delete_any) { - bulk_edit_delete_submit_button_text.each(function (index) { - let pretext = $(this).data('pretext'); - let posttext = $(this).data('posttext'); - $(this).text(`${pretext} ${bulk_edit_total_checked} ${posttext}`); - }); - } - - // Update update button state based on field selection - updateBulkEditButtonState(); - } - } let bulk_edit_picker_checkboxes = $('#bulk_edit_picker .update-needed'); bulk_edit_picker_checkboxes.on('click', function (e) { From c239e98622a49d52a6b29bc690ec9268a3d4e2db Mon Sep 17 00:00:00 2001 From: kodinkat Date: Wed, 11 Feb 2026 16:26:29 +0000 Subject: [PATCH 04/10] Enhance share field handling in modular-list-bulk.js - Updated share field logic to support multiple user selections, improving user experience. - Refactored user ID collection to accommodate both new and legacy formats, ensuring backward compatibility. - Enhanced initial value setup for share fields to handle arrays of user IDs, streamlining the bulk edit process. - Improved error handling for parsing user data from the share component, ensuring robustness in data retrieval. --- dt-assets/js/modular-list-bulk.js | 440 +++++++++++++++++++++--------- 1 file changed, 311 insertions(+), 129 deletions(-) diff --git a/dt-assets/js/modular-list-bulk.js b/dt-assets/js/modular-list-bulk.js index 6a179b25b7..ac75447913 100644 --- a/dt-assets/js/modular-list-bulk.js +++ b/dt-assets/js/modular-list-bulk.js @@ -479,19 +479,24 @@ (f) => f.fieldKey === 'share', ); if (shareFieldData) { - // Collect user ID from input + // Collect user IDs from component (supports multiple selections) const shareFieldWrapper = $( `.bulk-edit-field-wrapper[data-field-key="share"]`, ); - const shareUserId = collectFieldValue( + const shareUserIds = collectFieldValue( 'share', 'share', shareFieldWrapper, ); - if (shareUserId) { + if ( + shareUserIds && + Array.isArray(shareUserIds) && + shareUserIds.length > 0 + ) { + // Use legacy format for multiple users (backward compatible) sharePayload = { - user_id: shareUserId, - action: 'add', + users: shareUserIds, + unshare: false, }; } } @@ -763,36 +768,76 @@ // Special case: share field (not a real post field type) if (fieldType === 'share') { - const shareInputId = `bulk_share_${fieldKey}`; - const shareInput = fieldWrapper.find(`.js-typeahead-${shareInputId}`); + const shareComponentId = `bulk_share_${fieldKey}`; + const shareComponent = + fieldWrapper.find(`#${shareComponentId}`)[0] || + document.getElementById(shareComponentId); + + // Get fieldData to check if we have cached user IDs + const fieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); - if (shareInput.length > 0) { - // Get selected user ID from input data - let selectedUserId = shareInput.data('selected-user-id'); + // Return array of user IDs if available (supports multiple selections) + if ( + fieldData && + fieldData.shareUserIds && + fieldData.shareUserIds.length > 0 + ) { + return fieldData.shareUserIds; + } - // Fallback: check typeahead instance - if (!selectedUserId) { - const typeaheadSelector = `.js-typeahead-${shareInputId}`; - const typeaheadInstance = window.Typeahead?.[typeaheadSelector]; - if ( - typeaheadInstance && - typeaheadInstance.items && - typeaheadInstance.items.length > 0 - ) { - selectedUserId = typeaheadInstance.items[0].ID; + // Fallback: check component value directly + // dt-users-connection provides user IDs directly, so extract them + if (shareComponent && shareComponent.value) { + try { + let value; + if (typeof shareComponent.value === 'string') { + const trimmed = shareComponent.value.trim(); + if (trimmed === '' || trimmed === '[]' || trimmed === 'null') { + return null; + } + value = JSON.parse(shareComponent.value); + } else if (Array.isArray(shareComponent.value)) { + value = shareComponent.value; + } else { + return null; } - } - // Also store in fieldData for later use - const fieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - if (fieldData && selectedUserId) { - fieldData.shareUserId = selectedUserId; + if (Array.isArray(value) && value.length > 0) { + // Extract user IDs directly from dt-users-connection format + const userIds = value + .map((item) => { + const userId = item.id || item.user_id || null; + return userId ? parseInt(userId, 10) : null; + }) + .filter((id) => id !== null); + + if (userIds.length > 0) { + // Cache in fieldData for future use + if (fieldData) { + fieldData.shareUserIds = userIds; + fieldData.shareUserId = userIds[0]; + // Store labels if available + fieldData.shareUserLabels = {}; + value.forEach((item) => { + const userId = item.id || item.user_id; + if (userId) { + fieldData.shareUserLabels[userId] = item.label || ''; + } + }); + } + return userIds; + } + } + } catch (e) { + console.error( + 'Error parsing share component value in collectFieldValue:', + e, + ); } - - return selectedUserId || null; } + return null; } @@ -913,7 +958,7 @@ const component = fieldWrapper.find( 'dt-text, dt-textarea, dt-number, dt-toggle, dt-date, dt-single-select, ' + 'dt-multi-select, dt-multi-select-button-group, dt-multi-text, dt-tags, ' + - 'dt-connection, dt-location, dt-location-map, dt-user-select', + 'dt-connection, dt-users-connection, dt-location, dt-location-map, dt-user-select', )[0]; if (!component?.value) { @@ -1409,7 +1454,10 @@ fieldType: 'share', fieldName: window.wpApiShare?.translations?.share || 'Share', cleared: false, - shareUserId: null, // Store selected user ID for clear/restore + shareUserId: null, // Store selected user ID for clear/restore (backward compat) + shareUserIds: [], // Store array of selected user IDs (supports multiple) + shareUserLabel: null, // Store selected user label (backward compat) + shareUserLabels: {}, // Store user labels by user ID (supports multiple) }); // Render the share field @@ -1717,121 +1765,255 @@ // Handle share field specially if (fieldType === 'share') { - const shareInputId = `bulk_share_${fieldKey}`; - const shareResultContainerId = `bulk_share-result-container_${fieldKey}`; + const shareComponentId = `bulk_share_${fieldKey}`; + + // Get field data to check if we need to restore a previous selection + const fieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + const previousUserId = fieldData?.shareUserId || null; + + // Build initial value if we have previous user IDs (supports multiple) + // dt-users-connection format: [{id: , type: 'user', label: }] + let initialValue = '[]'; + if (fieldData?.shareUserIds && fieldData.shareUserIds.length > 0) { + // Use array of user IDs if available + initialValue = JSON.stringify( + fieldData.shareUserIds.map((userId) => ({ + id: userId, + type: 'user', + label: fieldData.shareUserLabels?.[userId] || '', + })), + ); + } else if (previousUserId) { + // Fallback to single user ID for backward compatibility + initialValue = JSON.stringify([ + { + id: previousUserId, + type: 'user', + label: fieldData?.shareUserLabel || '', + }, + ]); + } - // Build share HTML with typeahead input + // Build share HTML with dt-users-connection component + // This component is specifically designed for selecting system users let shareHtml = '
'; shareHtml += - ''; - shareHtml += - '
'; - shareHtml += '
'; - shareHtml += '
'; - shareHtml += ''; - shareHtml += - ''; - shareHtml += ''; - shareHtml += '
'; - shareHtml += '
'; - shareHtml += '
'; + '"; shareHtml += '
'; container.html(shareHtml); - // Initialize typeahead for share field + // Initialize component and set up value change listener requestAnimationFrame(() => { - const shareInput = $(`.js-typeahead-${shareInputId}`); - if (shareInput.length === 0) { + const shareComponent = document.getElementById(shareComponentId); + if (!shareComponent) { return; } - // Get field data to check if we need to restore a previous user selection - const fieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - const previousUserId = fieldData?.shareUserId || null; + // Initialize ComponentService if available + if (window.componentService && window.componentService.initialize) { + try { + window.componentService.initialize(); + } catch (e) { + // ComponentService initialization error - component should still work + } + } - // Initialize typeahead (adapted from shared-functions.js share typeahead) - const typeaheadSelector = `.js-typeahead-${shareInputId}`; - window.Typeahead[typeaheadSelector] = jQuery.typeahead({ - input: typeaheadSelector, - minLength: 0, - maxItem: 0, - accent: true, - searchOnFocus: true, - display: ['name'], - template: - '
' + - '' + - '' + - '' + - '
' + - '{{name}}' + - '{{subtitle}}' + - '
' + - '
', - emptyTemplate: window.SHAREDFUNCTIONS.escapeHTML( - window.wpApiShare?.translations?.no_records_found || - 'No records found', - ), - source: window.TYPEAHEADS.typeaheadUserSource(), - displayField: 'name', - matcher: window.TYPEAHEADS.typeaheadMatcher, - callback: { - onClick: function (node, a, item, event) { - // Get fresh reference to fieldData from the array (closure might have stale reference) - const currentFieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, + // Function to process component value and update fieldData + // dt-users-connection provides user IDs directly, so no conversion needed + const processShareComponentValue = async function (componentValue) { + const currentFieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + if (!currentFieldData) { + return; + } + + // Handle empty or invalid values + // componentValue might be a string, array, or object + if ( + !componentValue || + componentValue === null || + componentValue === undefined + ) { + // Value is null/undefined + currentFieldData.shareUserIds = []; + currentFieldData.shareUserLabels = {}; + currentFieldData.shareUserId = null; + currentFieldData.shareUserLabel = null; + return; + } + + // Handle if componentValue is already an array or object + let value; + if (Array.isArray(componentValue)) { + value = componentValue; + } else if (typeof componentValue === 'object') { + // If it's an object, try to convert to array format + value = Array.isArray(componentValue.value) + ? componentValue.value + : [componentValue]; + } else if (typeof componentValue === 'string') { + // Handle string values + const trimmed = componentValue.trim(); + if ( + trimmed === '' || + trimmed === 'null' || + trimmed === '[]' || + trimmed === 'undefined' + ) { + // Value is empty/null/empty array string + currentFieldData.shareUserIds = []; + currentFieldData.shareUserLabels = {}; + currentFieldData.shareUserId = null; + currentFieldData.shareUserLabel = null; + return; + } + // Try to parse as JSON + try { + value = JSON.parse(componentValue); + } catch (e) { + console.error( + 'Error parsing share component value as JSON:', + e, + 'Value was:', + componentValue, ); + // Clear field data on parse error + currentFieldData.shareUserIds = []; + currentFieldData.shareUserLabels = {}; + currentFieldData.shareUserId = null; + currentFieldData.shareUserLabel = null; + return; + } + } else { + // Unknown type + console.error( + 'Share component value has unexpected type:', + typeof componentValue, + componentValue, + ); + return; + } - // Store selected user ID in fieldData - if (currentFieldData) { - currentFieldData.shareUserId = item.ID; - } - // Store in input data for easy retrieval - shareInput.data('selected-user-id', item.ID); - shareInput.data('selected-user-name', item.name); - // Add visual feedback - shareInput.attr('data-selected', 'true'); - // Update input value to show selected user - shareInput.val(item.name); - - // Prevent default and hide layout - event.preventDefault(); - this.hideLayout(); - }, - onResult: function (node, query, result, resultCount) { - if (query === '') { - $(`#${shareResultContainerId}`).html(''); - } else { - let text = window.TYPEAHEADS.typeaheadHelpText( - resultCount, - query, - result, - ); - $(`#${shareResultContainerId}`).html(text); - } - }, - onHideLayout: function () { - $(`#${shareResultContainerId}`).html(''); - }, - }, + // Now process the value (should be an array at this point) + try { + if (Array.isArray(value) && value.length > 0) { + // dt-users-connection format: [{id: , type: 'user', label: }] + // Extract user IDs directly - no conversion needed! + const userIds = value + .map((item) => { + // item.id is the user ID directly + const userId = item.id || item.user_id || null; + return userId ? parseInt(userId, 10) : null; + }) + .filter((id) => id !== null); + + // Store user IDs and labels + currentFieldData.shareUserIds = userIds; + currentFieldData.shareUserLabels = {}; + value.forEach((item) => { + const userId = item.id || item.user_id; + if (userId) { + currentFieldData.shareUserLabels[userId] = item.label || ''; + } + }); + + // For backward compatibility, also store first user ID + currentFieldData.shareUserId = + userIds.length > 0 ? userIds[0] : null; + currentFieldData.shareUserLabel = + userIds.length > 0 + ? currentFieldData.shareUserLabels[userIds[0]] || '' + : null; + } else { + // Value is empty array or invalid format + currentFieldData.shareUserIds = []; + currentFieldData.shareUserLabels = {}; + currentFieldData.shareUserId = null; + currentFieldData.shareUserLabel = null; + } + } catch (e) { + // Invalid format - log error but don't crash + console.error( + 'Error processing share component value:', + e, + 'Value was:', + componentValue, + ); + // Clear field data on error + currentFieldData.shareUserIds = []; + currentFieldData.shareUserLabels = {}; + currentFieldData.shareUserId = null; + currentFieldData.shareUserLabel = null; + } + }; + + // Process initial value if present (fire and forget - will update fieldData asynchronously) + if (shareComponent.value) { + processShareComponentValue(shareComponent.value).catch((err) => { + // Silently handle errors - component should still work + }); + } + + // Listen for value changes to store user IDs (supports multiple selections) + shareComponent.addEventListener('change', async function () { + await processShareComponentValue(this.value); }); - // Restore previous selection if available + // If we have a previous user ID, restore it in the component if (previousUserId) { - shareInput.data('selected-user-id', previousUserId); - // Note: We don't know the previous user name here, so we leave the input value as-is - shareInput.attr('data-selected', 'true'); + // Fetch user details to set component value + fetch(`${window.wpApiShare.root}dt/v1/users/get_users?s=&get_all=1`, { + headers: { + 'X-WP-Nonce': window.wpApiShare.nonce, + }, + }) + .then((response) => response.json()) + .then((usersData) => { + const users = Array.isArray(usersData) + ? usersData + : usersData.posts || []; + const user = users.find( + (u) => u.ID === parseInt(previousUserId, 10), + ); + if (user && shareComponent) { + const value = JSON.stringify([ + { + id: user.ID, + type: 'user', + label: user.name || user.display_name || '', + }, + ]); + shareComponent.value = value; + // Update fieldData + const currentFieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + if (currentFieldData) { + currentFieldData.shareUserId = previousUserId; + currentFieldData.shareUserIds = [ + parseInt(previousUserId, 10), + ]; + currentFieldData.shareUserLabel = + user.name || user.display_name || ''; + currentFieldData.shareUserLabels = { + [previousUserId]: user.name || user.display_name || '', + }; + } + } + }) + .catch((err) => { + console.error('Error fetching user for restoration:', err); + }); } }); From aa372d431e6935f5de52790f95e4af85e837b07c Mon Sep 17 00:00:00 2001 From: kodinkat Date: Fri, 27 Feb 2026 11:12:06 +0000 Subject: [PATCH 05/10] Enhance bulk edit functionality in modular-list-bulk.js - Introduced a new utility function, normalizeShareComponentItems, to standardize the handling of share component values, improving robustness against various input types. - Refactored existing logic to utilize the new utility function, streamlining the process of extracting user IDs from share components. - Updated documentation to clarify the purpose of the module and the organization of bulk-related features. --- dt-assets/js/modular-list-bulk.js | 188 ++++++++++++------------------ 1 file changed, 72 insertions(+), 116 deletions(-) diff --git a/dt-assets/js/modular-list-bulk.js b/dt-assets/js/modular-list-bulk.js index 81056277f1..34fb7976e5 100644 --- a/dt-assets/js/modular-list-bulk.js +++ b/dt-assets/js/modular-list-bulk.js @@ -1,7 +1,11 @@ 'use strict'; /** * Bulk Edit Operations for Modular List - * Handles bulk edit, bulk delete, bulk messaging functionality + * Handles bulk edit, bulk delete, bulk messaging functionality. + * + * This module is the primary home for bulk behavior on modular lists. + * New bulk-related features (edit, delete, share/follow, messaging, app actions) + * should be implemented here rather than in modular-list.js. * * Organized into three main sections: * 1. Bulk Operations & List Controls - Checkbox handling, record counting, shared queue processing @@ -878,6 +882,41 @@ * Field Value Collection */ + function normalizeShareComponentItems(rawValue) { + if (rawValue === null || rawValue === undefined) { + return []; + } + + let value = rawValue; + + if (typeof rawValue === 'string') { + const trimmed = rawValue.trim(); + if (!trimmed || trimmed === 'null' || trimmed === '[]') { + return []; + } + try { + value = JSON.parse(trimmed); + } catch (e) { + console.error( + 'Error parsing share component string value as JSON:', + e, + 'Value was:', + rawValue, + ); + return []; + } + } else if (Array.isArray(rawValue)) { + value = rawValue; + } else if (typeof rawValue === 'object') { + value = Array.isArray(rawValue.value) ? rawValue.value : [rawValue]; + } else { + // Unexpected primitive type (number, boolean, etc.) + return []; + } + + return Array.isArray(value) ? value : []; + } + function collectFieldValue(fieldKey, fieldType, fieldWrapper) { // Special case: comment field (not a real post field type) if (fieldType === 'comment') { @@ -922,54 +961,37 @@ } // Fallback: check component value directly - // dt-users-connection provides user IDs directly, so extract them + // dt-users-connection should expose an array (or JSON string) of items if (shareComponent && shareComponent.value) { - try { - let value; - if (typeof shareComponent.value === 'string') { - const trimmed = shareComponent.value.trim(); - if (trimmed === '' || trimmed === '[]' || trimmed === 'null') { - return null; - } - value = JSON.parse(shareComponent.value); - } else if (Array.isArray(shareComponent.value)) { - value = shareComponent.value; - } else { - return null; - } + const items = normalizeShareComponentItems(shareComponent.value); + if (items.length === 0) { + return null; + } - if (Array.isArray(value) && value.length > 0) { - // Extract user IDs directly from dt-users-connection format - const userIds = value - .map((item) => { - const userId = item.id || item.user_id || null; - return userId ? parseInt(userId, 10) : null; - }) - .filter((id) => id !== null); - - if (userIds.length > 0) { - // Cache in fieldData for future use - if (fieldData) { - fieldData.shareUserIds = userIds; - fieldData.shareUserId = userIds[0]; - // Store labels if available - fieldData.shareUserLabels = {}; - value.forEach((item) => { - const userId = item.id || item.user_id; - if (userId) { - fieldData.shareUserLabels[userId] = item.label || ''; - } - }); - } - return userIds; + const userIds = items + .map((item) => { + const userId = item.id || item.user_id || null; + return userId ? parseInt(userId, 10) : null; + }) + .filter((id) => id !== null); + + if (userIds.length === 0) { + return null; + } + + if (fieldData) { + fieldData.shareUserIds = userIds; + fieldData.shareUserId = userIds[0]; + fieldData.shareUserLabels = {}; + items.forEach((item) => { + const userId = item.id || item.user_id; + if (userId) { + fieldData.shareUserLabels[userId] = item.label || ''; } - } - } catch (e) { - console.error( - 'Error parsing share component value in collectFieldValue:', - e, - ); + }); } + + return userIds; } return null; @@ -1960,7 +1982,7 @@ } // Function to process component value and update fieldData - // dt-users-connection provides user IDs directly, so no conversion needed + // dt-users-connection provides user IDs directly const processShareComponentValue = async function (componentValue) { const currentFieldData = bulkEditSelectedFields.find( (f) => f.fieldKey === fieldKey, @@ -1969,79 +1991,13 @@ return; } - // Handle empty or invalid values - // componentValue might be a string, array, or object - if ( - !componentValue || - componentValue === null || - componentValue === undefined - ) { - // Value is null/undefined - currentFieldData.shareUserIds = []; - currentFieldData.shareUserLabels = {}; - currentFieldData.shareUserId = null; - currentFieldData.shareUserLabel = null; - return; - } - - // Handle if componentValue is already an array or object - let value; - if (Array.isArray(componentValue)) { - value = componentValue; - } else if (typeof componentValue === 'object') { - // If it's an object, try to convert to array format - value = Array.isArray(componentValue.value) - ? componentValue.value - : [componentValue]; - } else if (typeof componentValue === 'string') { - // Handle string values - const trimmed = componentValue.trim(); - if ( - trimmed === '' || - trimmed === 'null' || - trimmed === '[]' || - trimmed === 'undefined' - ) { - // Value is empty/null/empty array string - currentFieldData.shareUserIds = []; - currentFieldData.shareUserLabels = {}; - currentFieldData.shareUserId = null; - currentFieldData.shareUserLabel = null; - return; - } - // Try to parse as JSON - try { - value = JSON.parse(componentValue); - } catch (e) { - console.error( - 'Error parsing share component value as JSON:', - e, - 'Value was:', - componentValue, - ); - // Clear field data on parse error - currentFieldData.shareUserIds = []; - currentFieldData.shareUserLabels = {}; - currentFieldData.shareUserId = null; - currentFieldData.shareUserLabel = null; - return; - } - } else { - // Unknown type - console.error( - 'Share component value has unexpected type:', - typeof componentValue, - componentValue, - ); - return; - } + const items = normalizeShareComponentItems(componentValue); - // Now process the value (should be an array at this point) try { - if (Array.isArray(value) && value.length > 0) { + if (items.length > 0) { // dt-users-connection format: [{id: , type: 'user', label: }] // Extract user IDs directly - no conversion needed! - const userIds = value + const userIds = items .map((item) => { // item.id is the user ID directly const userId = item.id || item.user_id || null; @@ -2052,7 +2008,7 @@ // Store user IDs and labels currentFieldData.shareUserIds = userIds; currentFieldData.shareUserLabels = {}; - value.forEach((item) => { + items.forEach((item) => { const userId = item.id || item.user_id; if (userId) { currentFieldData.shareUserLabels[userId] = item.label || ''; From 7b39a705e71445d082701c09ece9957be2eac41e Mon Sep 17 00:00:00 2001 From: kodinkat Date: Fri, 27 Feb 2026 12:09:30 +0000 Subject: [PATCH 06/10] Remove bulk delete functionality from modular-list.js and update related comments. The bulk delete logic has been relocated to modular-list-bulk.js to streamline the codebase and improve maintainability. --- dt-assets/js/modular-list.js | 915 ----------------------------------- 1 file changed, 915 deletions(-) diff --git a/dt-assets/js/modular-list.js b/dt-assets/js/modular-list.js index 58a1391cfe..0b12d721f9 100644 --- a/dt-assets/js/modular-list.js +++ b/dt-assets/js/modular-list.js @@ -3075,921 +3075,6 @@ * field selection, and submit handling */ - /*** - * Bulk Delete - */ - - let bulk_edit_delete_submit_button = $('#bulk_edit_delete_submit'); - bulk_edit_delete_submit_button.on('click', function (e) { - let bulk_edit_total_checked = $( - '.bulk_edit_checkbox:not(#bulk_edit_master) input:checked', - ).length; - - $('#bulk_edit_delete_submit-spinner').addClass('active'); - - if (bulk_edit_total_checked > 0) { - let bulk_edit_delete_submit_button_span = $( - '.bulk_edit_delete_submit_text', - ); - let confirm_text = `${$(bulk_edit_delete_submit_button_span).data('pretext')} ${bulk_edit_total_checked} ${$(bulk_edit_delete_submit_button_span).data('posttext')}`; - let confirm_text_capitalise = window.lodash.startCase( - window.lodash.toLower(confirm_text), - ); - - if ( - list_settings.permissions.delete_any && - confirm(confirm_text_capitalise) - ) { - bulk_delete_submit(); - } else { - window.location.reload(); - } - } else { - window.location.reload(); - } - }); - - function bulk_delete_submit() { - // Build queue of post ids. - let queue = []; - $('.bulk_edit_checkbox input').each(function () { - if (this.checked && this.id !== 'bulk_edit_master_checkbox') { - let postId = parseInt($(this).val()); - queue.push(postId); - } - }); - process(queue, 10, do_each, do_done, {}, null, {}, 'delete'); - } - - /** - * Bulk_Assigned_to - * NOTE: Bulk edit submit functionality has been moved to modular-list-bulk.js - */ - - let bulk_edit_picker_checkboxes = $('#bulk_edit_picker .update-needed'); - bulk_edit_picker_checkboxes.on('click', function (e) { - if ($(this).is(':checked')) { - $(this).data('bulk_key_requires_update', true); - } - }); - - let bulk_edit_picker_select_field = $('#bulk_edit_picker select'); - bulk_edit_picker_select_field.on('change', function (e) { - let field_key = this.id.replace('bulk_', ''); - $(this).data(`bulk_key_${field_key}`, this.value); - }); - - let bulk_edit_picker_button_groups = $('#bulk_edit_picker .select-button'); - bulk_edit_picker_button_groups.on('click', function (e) { - let field_key = $(this).data('field-key').replace('bulk_', ''); - let optionKey = $(this).attr('id'); - - let fieldValue = {}; - - fieldValue.values = { value: optionKey }; - - $(this).addClass('selected-select-button'); - $(this).data(`bulk_key_${field_key}`, fieldValue); - }); - - //Bulk Update Queue - function process( - q, - num, - fn, - done, - update, - share, - comment, - event_type = 'update', - responses = [], - ) { - // remove a batch of items from the queue - let items = q.splice(0, num), - count = items.length; - - // no more items? - if (!count) { - // exec done callback if specified - done && done(responses); - // quit - return; - } - - // loop over each item - for (let i = 0; i < count; i++) { - // call callback, passing item and - // a "done" callback - fn( - items[i], - function (response) { - // capture valid response - if (response) { - if (Array.isArray(response)) { - response.forEach((element) => responses.push(element)); - } else { - responses.push(response); - } - } - - // when done, decrement counter and - // if counter is 0, process next batch - --count || - process( - q, - num, - fn, - done, - update, - share, - comment, - event_type, - responses, - ); - }, - update, - share, - comment, - event_type, - ); - } - } - - // a per-item action - function do_each(item, done, update, share, comment, event_type) { - let promises = []; - - switch (event_type) { - case 'update': { - if (Object.keys(update).length) { - promises.push( - window.API.update_post(list_settings.post_type, item, update).catch( - (err) => { - // Error handled silently - user will see error via UI feedback - }, - ), - ); - } - - // Handle new share payload format: { user_id: , action: 'add' } - if (share && share.user_id && share.action === 'add') { - promises.push( - window.API.add_shared( - list_settings.post_type, - item, - share.user_id, - ).catch((err) => { - console.error(err); - }), - ); - } - // Legacy share format support (backward compatibility) - else if (share && share['users']) { - share['users'].forEach(function (value) { - let promise = share['unshare'] - ? window.API.remove_shared( - list_settings.post_type, - item, - value, - ).catch((err) => { - console.error(err); - }) - : window.API.add_shared( - list_settings.post_type, - item, - value, - ).catch((err) => { - console.error(err); - }); - promises.push(promise); - }); - } - - if (comment.commentText) { - promises.push( - window.API.post_comment( - list_settings.post_type, - item, - comment.commentText, - comment.commentType, - ).catch((err) => { - console.error(err); - }), - ); - } - - break; - } - case 'delete': { - if (list_settings.permissions.delete_any) { - promises.push( - window.API.delete_post(list_settings.post_type, item).catch( - (err) => { - console.error(err); - }, - ), - ); - } - break; - } - case 'message': { - if ( - update?.subject && - update?.from_name && - update?.send_method && - update?.message - ) { - promises.push( - window - .makeRequestOnPosts( - 'POST', - `${list_settings.post_type}/${item}/post_messaging`, - update, - ) - .catch((err) => { - console.error(err); - }), - ); - } - break; - } - } - Promise.all(promises).then(function (responses) { - done(responses); - }); - } - - function do_done() { - $('#bulk_edit_submit-spinner').removeClass('active'); - $('#bulk_edit_delete_submit-spinner').removeClass('active'); - window.location.reload(); - } - - let bulk_assigned_to_input = $(`.js-typeahead-bulk_assigned_to`); - if (bulk_assigned_to_input.length) { - $.typeahead({ - input: '.js-typeahead-bulk_assigned_to', - minLength: 0, - maxItem: 0, - accent: true, - searchOnFocus: true, - source: window.TYPEAHEADS.typeaheadUserSource(), - templateValue: '{{name}}', - template: function (query, item) { - return `
- - - ${window.SHAREDFUNCTIONS.escapeHTML(item.name)} - - ${item.status_color ? ` ` : ''} - ${ - item.update_needed && item.update_needed > 0 - ? ` - - ${window.SHAREDFUNCTIONS.escapeHTML(item.update_needed)} - ` - : '' - } -
`; - }, - dynamic: true, - hint: true, - emptyTemplate: window.SHAREDFUNCTIONS.escapeHTML( - window.wpApiShare.translations.no_records_found, - ), - callback: { - onClick: function (node, a, item) { - node.data('bulk_key_assigned_to', `user-${item.ID}`); - }, - onResult: function (node, query, result, resultCount) { - let text = window.TYPEAHEADS.typeaheadHelpText( - resultCount, - query, - result, - ); - $('#bulk_assigned_to-result-container').html(text); - }, - onHideLayout: function () { - $('.bulk_assigned_to-result-container').html(''); - }, - onReady: function () {}, - }, - }); - } - - /** - * Bulk Send Message - */ - - $('#bulk_send_msg_submit').on('click', function (e) { - handle_bulk_send_messages(); - }); - - function handle_bulk_send_messages() { - const spinner = $('#bulk_send_msg_submit-spinner'); - - let subject = $('#bulk_send_msg_subject').val().trim(); - let from_name = $('#bulk_send_msg_from_name').val().trim(); - let reply_to = $('#bulk_send_msg_reply_to').val().trim(); - let send_method = 'email'; - let message = $('#bulk_send_msg').val().trim(); - - // If multiple options detected, ensure correct selection is made. - const checked_send_method = $('.bulk-send-msg-method:checked'); - if ($(checked_send_method).length > 0) { - send_method = $(checked_send_method).val().trim(); - } - - let queue = []; - $('.bulk_edit_checkbox input').each(function () { - if (this.checked && this.id !== 'bulk_edit_master_checkbox') { - queue.push(parseInt($(this).val())); - } - }); - - // Validate entries. - if (!subject) { - $('#bulk_send_msg_subject_support_text').show(); - return; - } else { - $('#bulk_send_msg_subject_support_text').hide(); - } - - if (!from_name) { - $('#bulk_send_msg_from_name_support_text').show(); - return; - } else { - $('#bulk_send_msg_from_name_support_text').hide(); - } - - if (!send_method) { - $('#bulk_send_msg_method_support_text').show(); - return; - } else { - $('#bulk_send_msg_method_support_text').hide(); - } - - if (!message) { - $('#bulk_send_msg_support_text').show(); - return; - } else { - $('#bulk_send_msg_support_text').hide(); - } - - if (!queue || queue.length < 1) { - $('#bulk_send_msg_submit_support_text').show(); - return; - } else { - $('#bulk_send_msg_submit_support_text').hide(); - } - - // Proceed with staged-based message send requests. - $(spinner).addClass('active'); - process( - queue, - 10, - do_each, - function (responses) { - // If available, extract response summary. - if (responses && responses.length > 0) { - let email_queue_link = `${list_settings.translations.see_queue}`; - let count_sent = 0; - let count_fails = 0; - responses.forEach(function (response) { - if (response && response['sent'] !== undefined) { - if (response['sent']) { - count_sent++; - } else { - count_fails++; - } - } - }); - - $('#bulk_send_msg_submit-message').html( - `${count_sent} ${list_settings.translations.sent}! ${window.wpApiShare.can_manage_dt ? email_queue_link : ''}
${count_fails} ${list_settings.translations.not_sent}`, - ); - } - - // Reset record selections. - $(spinner).removeClass('active'); - $('#bulk_edit_master_checkbox').prop('checked', false); - $('.bulk_edit_checkbox input').prop('checked', false); - bulk_edit_count(); - // window.location.reload(); - }, - { - subject: subject, - from_name: from_name, - reply_to: reply_to, - send_method: send_method, - message: message, - }, - {}, - {}, - 'message', - ); - } - - /** - * Bulk share (only initialize if element exists - field may be dynamically added) - */ - let bulk_share_input = $('#bulk_share'); - if (bulk_share_input.length) { - $.typeahead({ - input: '#bulk_share', - minLength: 0, - maxItem: 0, - accent: true, - searchOnFocus: true, - source: window.TYPEAHEADS.typeaheadUserSource(), - templateValue: '{{name}}', - dynamic: true, - multiselect: { - matchOn: ['ID'], - callback: { - onCancel: function (node, item) { - $(node).removeData(`bulk_key_bulk_share`); - $('#share-result-container').html(''); - }, - }, - }, - callback: { - onClick: function (node, a, item, event) { - let shareUserArray; - if (node.data('bulk_key_share')) { - shareUserArray = node.data('bulk_key_share'); - } else { - shareUserArray = []; - } - shareUserArray.push(item.ID); - node.data(`bulk_key_share`, shareUserArray); - }, - onResult: function (node, query, result, resultCount) { - if (query) { - let text = window.TYPEAHEADS.typeaheadHelpText( - resultCount, - query, - result, - ); - $('#share-result-container').html(text); - } - }, - onHideLayout: function () { - $('#share-result-container').html(''); - }, - }, - }); - } - - /** - * Bulk Typeahead - */ - let field_settings = window.list_settings.post_type_settings.fields; - - $('#bulk_edit_picker .dt_typeahead').each((key, el) => { - let element_id = $(el) - .attr('id') - .replace(/_connection$/, ''); - let div_id = $(el).attr('id'); - let field_id = $(`#${div_id} input`).data('field'); - if (element_id !== 'bulk_share') { - let listing_post_type = window.lodash.get( - window.list_settings.post_type_settings.fields[field_id], - 'post_type', - 'contacts', - ); - $.typeahead({ - input: `.js-typeahead-${element_id}`, - minLength: 0, - accent: true, - maxItem: 30, - searchOnFocus: true, - template: window.TYPEAHEADS.contactListRowTemplate, - source: window.TYPEAHEADS.typeaheadPostsSource(listing_post_type, { - field_key: field_id, - }), - display: 'name', - templateValue: '{{name}}', - dynamic: true, - multiselect: { - matchOn: ['ID'], - data: '', - callback: { - onCancel: function (node, item) { - $(node).removeData(`bulk_key_${field_id}`); - }, - }, - href: window.wpApiShare.site_url + `/${listing_post_type}/{{ID}}`, - }, - callback: { - onClick: function (node, a, item, event) { - let multiUserArray; - if (node.data(`bulk_key_${field_id}`)) { - multiUserArray = node.data(`bulk_key_${field_id}`).values; - } else { - multiUserArray = []; - } - multiUserArray.push({ value: item.ID }); - - node.data(`bulk_key_${field_id}`, { values: multiUserArray }); - this.addMultiselectItemLayout(item); - event.preventDefault(); - this.hideLayout(); - this.resetInput(); - }, - onResult: function (node, query, result, resultCount) { - let text = window.TYPEAHEADS.typeaheadHelpText( - resultCount, - query, - result, - ); - $(`#${element_id}-result-container`).html(text); - }, - onHideLayout: function (event, query) { - if (!query) { - $(`#${element_id}-result-container`).empty(); - } - }, - onShowLayout() {}, - }, - }); - } - }); - - if ($('#bulk_edit_picker .js-typeahead-bulk_location_grid').length) { - $('#bulk_edit_picker .dt_location_grid').each(() => { - let field_id = 'location_grid'; - let typeaheadTotals = {}; - $.typeahead({ - input: '.js-typeahead-bulk_location_grid', - minLength: 0, - accent: true, - searchOnFocus: true, - maxItem: 20, - dropdownFilter: [ - { - key: 'group', - value: 'focus', - template: window.SHAREDFUNCTIONS.escapeHTML( - window.wpApiShare.translations.regions_of_focus, - ), - all: window.SHAREDFUNCTIONS.escapeHTML( - window.wpApiShare.translations.all_locations, - ), - }, - ], - source: { - focus: { - display: 'name', - ajax: { - url: - window.wpApiShare.root + - 'dt/v1/mapping_module/search_location_grid_by_name', - data: { - s: '{{query}}', - filter: function () { - // return window.lodash.get(window.Typeahead['.js-typeahead-location_grid'].filters.dropdown, 'value', 'all') - }, - }, - beforeSend: function (xhr) { - xhr.setRequestHeader('X-WP-Nonce', window.wpApiShare.nonce); - }, - callback: { - done: function (data) { - if (typeof window.typeaheadTotals !== 'undefined') { - window.typeaheadTotals.field = data.total; - } - return data.location_grid; - }, - }, - }, - }, - }, - display: 'name', - templateValue: '{{name}}', - dynamic: true, - multiselect: { - matchOn: ['ID'], - data: '', - callback: { - onCancel: function (node, item) { - $(node).removeData(`bulk_key_${field_id}`); - }, - }, - }, - callback: { - onClick: function (node, a, item, event) { - // $(`#${element_id}-spinner`).addClass('active'); - node.data(`bulk_key_${field_id}`, { values: [{ value: item.ID }] }); - }, - onReady() { - this.filters.dropdown = { - key: 'group', - value: 'focus', - template: window.SHAREDFUNCTIONS.escapeHTML( - window.wpApiShare.translations.regions_of_focus, - ), - }; - this.container - .removeClass('filter') - .find('.' + this.options.selector.filterButton) - .html( - window.SHAREDFUNCTIONS.escapeHTML( - window.wpApiShare.translations.regions_of_focus, - ), - ); - }, - onResult: function (node, query, result, resultCount) { - resultCount = typeaheadTotals.location_grid; - let text = window.TYPEAHEADS.typeaheadHelpText( - resultCount, - query, - result, - ); - $('#location_grid-result-container').html(text); - }, - onHideLayout: function () { - $('#location_grid-result-container').html(''); - }, - }, - }); - }); - } - - $( - '#bulk_edit_picker .tags input, #bulk_edit_picker .multi_select input', - ).each((key, input) => { - let field = $(input).data('field') || 'tags'; - let field_options = window.lodash.get( - list_settings, - `post_type_settings.fields.${field}.default`, - {}, - ); - $.typeahead({ - input: input, - minLength: 0, - maxItem: 20, - searchOnFocus: true, - source: { - tags: { - display: ['name'], - ajax: { - url: - window.wpApiShare.root + - `dt-posts/v2/${list_settings.post_type}/multi-select-values`, - data: { - s: '{{query}}', - field: field, - }, - beforeSend: function (xhr) { - xhr.setRequestHeader('X-WP-Nonce', window.wpApiShare.nonce); - }, - callback: { - done: function (data) { - return (data || []).map((tag) => { - let label = window.lodash.get( - field_options, - tag + '.label', - tag, - ); - return { name: label || tag, key: tag }; - }); - }, - }, - }, - }, - }, - display: 'name', - templateValue: '{{name}}', - dynamic: true, - multiselect: { - matchOn: ['key'], - callback: { - onCancel: function (node, item) { - $(node).removeData(`bulk_key_${field}`); - }, - }, - }, - callback: { - onClick: function (node, a, item, event) { - let multiUserArray; - if (node.data(`bulk_key_${field}`)) { - multiUserArray = node.data(`bulk_key_${field}`).values; - } else { - multiUserArray = []; - } - multiUserArray.push({ value: item.key }); - - node.data(`bulk_key_${field}`, { values: multiUserArray }); - this.addMultiselectItemLayout(item); - event.preventDefault(); - this.hideLayout(); - this.resetInput(); - }, - onResult: function (node, query, result, resultCount) { - let text = window.TYPEAHEADS.typeaheadHelpText( - resultCount, - query, - result, - ); - $(`#${field}-result-container`).html(text); - }, - onHideLayout: function () { - $(`#${field}-result-container`).html(''); - }, - }, - }); - }); - - $('button.follow').on('click', function () { - let following = !($(this).data('value') === 'following'); - $(this).data('value', following ? 'following' : ''); - $(this).html(following ? 'Unfollow' : 'Follow'); - $(this).toggleClass('hollow'); - let follow = { values: [{ value: current_user_id, delete: !following }] }; - - let unfollow = { values: [{ value: current_user_id, delete: following }] }; - - $(this).data('bulk_key_follow', follow); - $(this).data('bulk_key_unfollow', unfollow); - }); - - $('#bulk_edit_picker input.text-input').change(function () { - const val = $(this).val(); - let field_key = this.id.replace('bulk_', ''); - $(this).data(`bulk_key_${field_key}`, val); - }); - - $('#bulk_edit_picker .dt_textarea').change(function () { - const val = $(this).val(); - let field_key = this.id.replace('bulk_', ''); - $(this).data(`bulk_key_${field_key}`, val); - }); - - $('#bulk_edit_picker .dt_date_picker') - .datepicker({ - constrainInput: false, - dateFormat: 'yy-mm-dd', - onClose: function (date) { - date = window.SHAREDFUNCTIONS.convertArabicToEnglishNumbers(date); - - if (!$(this).val()) { - date = ' '; //null; - } - - let formattedDate = window.moment.utc(date).unix(); - - let field_key = this.id.replace('bulk_', ''); - $(this).data(`bulk_key_${field_key}`, formattedDate); - }, - changeMonth: true, - changeYear: true, - yearRange: '1900:2050', - }) - .each(function () { - if (this.value && window.moment.unix(this.value).isValid()) { - this.value = window.SHAREDFUNCTIONS.formatDate(this.value); - } - }); - - let mcleardate = $('#bulk_edit_picker .clear-date-button'); - mcleardate.click(function () { - let input_id = this.dataset.inputid; - let date = null; - // $(`#${input_id}-spinner`).addClass('active') - let field_key = this.id.replace('bulk_', ''); - $(this).removeData(`bulk_key_${field_key}`); - $(`#${input_id}`).val(''); - }); - - $('#bulk_edit_picker dt-single-select').change((e) => { - const val = $(e.currentTarget).val(); - - if (val === 'paused') { - $('#bulk_reason_paused').parent().toggle(); - } - }); - - $('#bulk_edit_picker input.number-input').on('blur', function () { - const id = $(this).attr('id'); - const val = $(this).val(); - - let field_key = this.id.replace('bulk_', ''); - $(this).data(`bulk_key_${field_key}`, val); - }); - - $('#bulk_edit_picker .dt_contenteditable').on('blur', function () { - const id = $(this).attr('id'); - let val = $(this).html(); - - let field_key = this.id.replace('bulk_', ''); - $(this).data(`bulk_key_${field_key}`, val); - }); - - $('.list-dropdown-submenu-item-link').on('click', function () { - // Hide bulk select modals - $('#records-table').removeClass('bulk_edit_on'); - - // Close all open modals - $('.list-dropdown-submenu-item-link').each(function () { - let open_modals = $(this).data('modal'); - $('#' + open_modals).hide(); - }); - - // Open modal for clicked menu item - let display_modal = $(this).data('modal'); - $('#' + display_modal).show(); - - // Show bulk select checkboxes if applicable - if ($(this).data('checkboxes') === true) { - $('#records-table').addClass('bulk_edit_on'); - } - }); - - $('.list-action-close-button').on('click', function () { - let section = $(this).data('close'); - if (section) { - $(`#${section}`).hide(); - if (section === 'bulk_edit_picker') { - $('#records-table').toggleClass('bulk_edit_on'); - } - } else { - $('#list-actions .list_action_section').hide(); - $('#records-table').removeClass('bulk_edit_on'); - } - }); - - /***** - * Bulk Send App - */ - let bulk_send_app_button = $('#bulk_send_app_submit'); - bulk_send_app_button.on('click', function (e) { - bulk_send_app(); - }); - - function bulk_send_app() { - let subject = $('#bulk_send_app_subject').val(); - let note = $('#bulk_send_app_msg').val(); - - let selected_input = jQuery( - '.bulk_send_app.dt-radio.button-group input:checked', - ); - if (selected_input.length < 1) { - $('#bulk_send_app_required_selection').show(); - return; - } else { - $('#bulk_send_app_required_selection').hide(); - } - - let root = selected_input.data('root'); - let type = selected_input.data('type'); - - let queue = []; - $('.bulk_edit_checkbox input').each(function () { - if (this.checked && this.id !== 'bulk_edit_master_checkbox') { - let postId = parseInt($(this).val()); - queue.push(postId); - } - }); - - if (queue.length < 1) { - $('#bulk_send_app_required_elements').show(); - return; - } else { - $('#bulk_send_app_required_elements').hide(); - } - - $('#bulk_send_app_submit-spinner').addClass('active'); - - let email_queue_link = `See queue`; - - window - .makeRequest('POST', list_settings.post_type + '/email_magic', { - root: root, - type: type, - subject: subject, - note: note, - post_ids: queue, - }) - .done((data) => { - $('#bulk_send_app_submit-spinner').removeClass('active'); - $('#bulk_send_app_submit-message').html( - `${data.total_sent} ${list_settings.translations.sent}! ${window.wpApiShare.can_manage_dt ? email_queue_link : ''}
${data.total_unsent} ${list_settings.translations.not_sent}`, - ); - $('#bulk_edit_master_checkbox').prop('checked', false); - $('.bulk_edit_checkbox input').prop('checked', false); - bulk_edit_count(); - // window.location.reload(); - }) - .fail((e) => { - $('#bulk_send_app_submit-spinner').removeClass('active'); - $('#bulk_send_app_submit-message').html( - 'Oops. Something went wrong! Check log.', - ); - console.log(e); - }); - } - /** * Split By Feature */ From 4e79e17371fec93236e2c63dbf57be6a97406998 Mon Sep 17 00:00:00 2001 From: kodinkat Date: Fri, 27 Feb 2026 12:56:31 +0000 Subject: [PATCH 07/10] Refactor share component handling in modular-list-bulk.js - Simplified the logic for checking and processing share field data, improving readability and maintainability. - Updated the processShareComponentValue function to handle errors more gracefully and ensure compatibility with existing data structures. - Enhanced event listener for share component changes to better manage asynchronous updates and error logging. --- dt-assets/js/modular-list-bulk.js | 212 +++++++++++++----------------- 1 file changed, 91 insertions(+), 121 deletions(-) diff --git a/dt-assets/js/modular-list-bulk.js b/dt-assets/js/modular-list-bulk.js index 34fb7976e5..343059f600 100644 --- a/dt-assets/js/modular-list-bulk.js +++ b/dt-assets/js/modular-list-bulk.js @@ -558,34 +558,29 @@ let sharePayload = null; // Check for share field in dynamically selected fields - const shareFieldSelected = bulkEditSelectedFields?.some( + const shareFieldData = bulkEditSelectedFields?.find( (f) => f.fieldKey === 'share', ); - if (shareFieldSelected) { - const shareFieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === 'share', + if (shareFieldData) { + // Collect user IDs from component (supports multiple selections) + const shareFieldWrapper = $( + `.bulk-edit-field-wrapper[data-field-key="share"]`, ); - if (shareFieldData) { - // Collect user IDs from component (supports multiple selections) - const shareFieldWrapper = $( - `.bulk-edit-field-wrapper[data-field-key="share"]`, - ); - const shareUserIds = collectFieldValue( - 'share', - 'share', - shareFieldWrapper, - ); - if ( - shareUserIds && - Array.isArray(shareUserIds) && - shareUserIds.length > 0 - ) { - // Use legacy format for multiple users (backward compatible) - sharePayload = { - users: shareUserIds, - unshare: false, - }; - } + const shareUserIds = collectFieldValue( + 'share', + 'share', + shareFieldWrapper, + ); + if ( + shareUserIds && + Array.isArray(shareUserIds) && + shareUserIds.length > 0 + ) { + // Use legacy format for multiple users (backward compatible) + sharePayload = { + users: shareUserIds, + unshare: false, + }; } } @@ -1039,7 +1034,7 @@ // Get current user ID const currentUserId = - window.wpApiNotifications?.current_user_id || current_user_id; + window.wpApiNotifications?.current_user_id || DT_List.current_user_id; if (currentUserId) { // Return follow/unfollow structure @@ -1983,7 +1978,7 @@ // Function to process component value and update fieldData // dt-users-connection provides user IDs directly - const processShareComponentValue = async function (componentValue) { + function processShareComponentValue(componentValue) { const currentFieldData = bulkEditSelectedFields.find( (f) => f.fieldKey === fieldKey, ); @@ -1993,115 +1988,90 @@ const items = normalizeShareComponentItems(componentValue); - try { - if (items.length > 0) { - // dt-users-connection format: [{id: , type: 'user', label: }] - // Extract user IDs directly - no conversion needed! - const userIds = items - .map((item) => { - // item.id is the user ID directly - const userId = item.id || item.user_id || null; - return userId ? parseInt(userId, 10) : null; - }) - .filter((id) => id !== null); - - // Store user IDs and labels - currentFieldData.shareUserIds = userIds; - currentFieldData.shareUserLabels = {}; - items.forEach((item) => { - const userId = item.id || item.user_id; - if (userId) { - currentFieldData.shareUserLabels[userId] = item.label || ''; - } - }); + if (items.length > 0) { + // dt-users-connection format: [{id: , type: 'user', label: }] + // Extract user IDs directly - no conversion needed! + const userIds = items + .map((item) => { + // item.id is the user ID directly + const userId = item.id || item.user_id || null; + return userId ? parseInt(userId, 10) : null; + }) + .filter((id) => id !== null); + + // Store user IDs and labels + currentFieldData.shareUserIds = userIds; + currentFieldData.shareUserLabels = {}; + items.forEach((item) => { + const userId = item.id || item.user_id; + if (userId) { + currentFieldData.shareUserLabels[userId] = item.label || ''; + } + }); - // For backward compatibility, also store first user ID - currentFieldData.shareUserId = - userIds.length > 0 ? userIds[0] : null; - currentFieldData.shareUserLabel = - userIds.length > 0 - ? currentFieldData.shareUserLabels[userIds[0]] || '' - : null; - } else { - // Value is empty array or invalid format - currentFieldData.shareUserIds = []; - currentFieldData.shareUserLabels = {}; - currentFieldData.shareUserId = null; - currentFieldData.shareUserLabel = null; - } - } catch (e) { - // Invalid format - log error but don't crash - console.error( - 'Error processing share component value:', - e, - 'Value was:', - componentValue, - ); - // Clear field data on error + // For backward compatibility, also store first user ID + currentFieldData.shareUserId = + userIds.length > 0 ? userIds[0] : null; + currentFieldData.shareUserLabel = + userIds.length > 0 + ? currentFieldData.shareUserLabels[userIds[0]] || '' + : null; + } else { + // Value is empty array or invalid format currentFieldData.shareUserIds = []; currentFieldData.shareUserLabels = {}; currentFieldData.shareUserId = null; currentFieldData.shareUserLabel = null; } - }; + } - // Process initial value if present (fire and forget - will update fieldData asynchronously) + // Process initial value if present if (shareComponent.value) { - processShareComponentValue(shareComponent.value).catch((err) => { - // Silently handle errors - component should still work - }); + try { + processShareComponentValue(shareComponent.value); + } catch (err) { + console.error('Error processing initial share value:', err); + } } // Listen for value changes to store user IDs (supports multiple selections) - shareComponent.addEventListener('change', async function () { - await processShareComponentValue(this.value); + shareComponent.addEventListener('change', function () { + try { + processShareComponentValue(this.value); + } catch (err) { + console.error('Error processing share change value:', err); + } }); // If we have a previous user ID, restore it in the component if (previousUserId) { - // Fetch user details to set component value - fetch(`${window.wpApiShare.root}dt/v1/users/get_users?s=&get_all=1`, { - headers: { - 'X-WP-Nonce': window.wpApiShare.nonce, - }, - }) - .then((response) => response.json()) - .then((usersData) => { - const users = Array.isArray(usersData) - ? usersData - : usersData.posts || []; - const user = users.find( - (u) => u.ID === parseInt(previousUserId, 10), - ); - if (user && shareComponent) { - const value = JSON.stringify([ - { - id: user.ID, - type: 'user', - label: user.name || user.display_name || '', - }, - ]); - shareComponent.value = value; - // Update fieldData - const currentFieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - if (currentFieldData) { - currentFieldData.shareUserId = previousUserId; - currentFieldData.shareUserIds = [ - parseInt(previousUserId, 10), - ]; - currentFieldData.shareUserLabel = - user.name || user.display_name || ''; - currentFieldData.shareUserLabels = { - [previousUserId]: user.name || user.display_name || '', - }; - } - } - }) - .catch((err) => { - console.error('Error fetching user for restoration:', err); - }); + const label = + fieldData?.shareUserLabels?.[previousUserId] || + fieldData?.shareUserLabel || + ''; + + if (label && shareComponent) { + const value = JSON.stringify([ + { + id: parseInt(previousUserId, 10), + type: 'user', + label, + }, + ]); + shareComponent.value = value; + // Update fieldData + const currentFieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + if (currentFieldData) { + currentFieldData.shareUserId = previousUserId; + currentFieldData.shareUserIds = [parseInt(previousUserId, 10)]; + currentFieldData.shareUserLabel = label; + currentFieldData.shareUserLabels = { + [previousUserId]: label, + }; + } + } } }); From cd3f20085b600cb2efba87c02a8728e5768cf9ac Mon Sep 17 00:00:00 2001 From: kodinkat Date: Fri, 27 Feb 2026 13:19:48 +0000 Subject: [PATCH 08/10] Update comments and refactor toggle handling in modular-list-bulk.js - Clarified comments regarding share payload formats and the use of legacy formats for multiple users. - Updated the toggle handling for clearable fields to use a more descriptive class name. - Enhanced the follow HTML generation to escape label and help text, improving security against HTML injection. - Delegated the 'Remove Values' mode toggle to ensure proper handling of clearable fields. --- dt-assets/js/modular-list-bulk.js | 38 +++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/dt-assets/js/modular-list-bulk.js b/dt-assets/js/modular-list-bulk.js index 343059f600..c56518ec80 100644 --- a/dt-assets/js/modular-list-bulk.js +++ b/dt-assets/js/modular-list-bulk.js @@ -290,7 +290,7 @@ } } - // Handle new share payload format: { user_id: , action: 'add' } + // Single-user add format (reserved for future use); new share UI uses legacy format below if (share && share.user_id && share.action === 'add') { promises.push( window.API.add_shared( @@ -576,7 +576,8 @@ Array.isArray(shareUserIds) && shareUserIds.length > 0 ) { - // Use legacy format for multiple users (backward compatible) + // Use legacy format for multiple users (backward compatible). + // Bulk unshare via this component is out of scope for now; unshare remains false. sharePayload = { users: shareUserIds, unshare: false, @@ -1741,12 +1742,13 @@ const inputContainer = wrapper.find('.bulk-edit-field-input-container'); renderBulkEditFieldInput(fieldKey, fieldType, inputContainer); - // Show clear button for fields that support clearing (exclude comment, follow, and share fields) + // Show "Remove Values" mode toggle for clearable fields (exclude comment, follow, and share) + // Template uses .bulk-edit-mode-toggle (dt-toggle), not .bulk-edit-clear-field-btn if ( supportsFieldClearing(fieldType) && !['comment', 'share', 'follow'].includes(fieldType) ) { - wrapper.find('.bulk-edit-clear-field-btn').show(); + wrapper.find('.bulk-edit-mode-toggle').show(); } // Append to container @@ -2082,20 +2084,23 @@ if (fieldType === 'follow') { const followToggleId = `bulk_follow_${fieldKey}`; - // Build follow HTML with dt-toggle component + // Build follow HTML with dt-toggle component (escape label/help-text to prevent attribute/HTML injection) + const followLabel = window.SHAREDFUNCTIONS.escapeHTML( + window.wpApiShare?.translations?.follow || 'Follow', + ); + const followHelpText = window.SHAREDFUNCTIONS.escapeHTML( + window.wpApiShare?.translations?.follow_help || + 'Toggle to follow or unfollow records', + ); let followHtml = '
'; followHtml += ''; + '" label="' + + followLabel + + '" help-text="' + + followHelpText + + '">'; followHtml += '
'; container.html(followHtml); @@ -2515,6 +2520,11 @@ updateBulkEditButtonState(); } + // Delegate "Remove Values" mode toggle (dt-toggle) so handleModeToggleChange runs for clearable fields + $(document).on('change', '.bulk-edit-mode-toggle', function () { + handleModeToggleChange(this); + }); + // Remove field when clicking X button $(document).on('click', '.bulk-edit-remove-field-btn', function () { const fieldKey = $(this).data('field-key'); From e086940c50c4b920dc7afef8bdb95198cc855a8e Mon Sep 17 00:00:00 2001 From: kodinkat Date: Fri, 27 Feb 2026 13:33:34 +0000 Subject: [PATCH 09/10] Enhance share functionality in modular-list-bulk.js and enqueue-scripts.php - Added new translation strings for 'Share', 'Follow', and 'Follow Help' in enqueue-scripts.php to improve user interface clarity. - Refactored the processShareComponentValue function in modular-list-bulk.js to streamline the handling of share component values, ensuring compatibility with various input formats. - Improved error handling and logging for component initialization and value processing, enhancing robustness and maintainability. --- dt-assets/functions/enqueue-scripts.php | 3 + dt-assets/js/modular-list-bulk.js | 145 ++++++++++-------------- 2 files changed, 61 insertions(+), 87 deletions(-) diff --git a/dt-assets/functions/enqueue-scripts.php b/dt-assets/functions/enqueue-scripts.php index 2956ffd481..c6a1fd2e18 100644 --- a/dt-assets/functions/enqueue-scripts.php +++ b/dt-assets/functions/enqueue-scripts.php @@ -136,6 +136,9 @@ function dt_site_scripts() { 'edit' => __( 'Edit', 'disciple_tools' ), 'copy' => __( 'Copy', 'disciple_tools' ), 'copied_text' => __( 'Copied: %s', 'disciple_tools' ), + 'share' => __( 'Share', 'disciple_tools' ), + 'follow' => __( 'Follow', 'disciple_tools' ), + 'follow_help' => __( 'Toggle to follow or unfollow records', 'disciple_tools' ), ], 'post_type' => $post_type, 'url_path' => $url_path, diff --git a/dt-assets/js/modular-list-bulk.js b/dt-assets/js/modular-list-bulk.js index c56518ec80..7b055e334e 100644 --- a/dt-assets/js/modular-list-bulk.js +++ b/dt-assets/js/modular-list-bulk.js @@ -290,7 +290,7 @@ } } - // Single-user add format (reserved for future use); new share UI uses legacy format below + // Single-user add format { user_id, action: 'add' } — reserved for future use; no code path produces it yet (new share UI uses legacy { users, unshare } below) if (share && share.user_id && share.action === 'add') { promises.push( window.API.add_shared( @@ -878,6 +878,11 @@ * Field Value Collection */ + /** + * Normalize dt-users-connection value into an array of items. + * In practice the component typically provides a JSON string of [{id, label}] or an array; + * other branches (string 'null'/'[]', object .value, primitives) are defensive for robustness. + */ function normalizeShareComponentItems(rawValue) { if (rawValue === null || rawValue === undefined) { return []; @@ -913,6 +918,49 @@ return Array.isArray(value) ? value : []; } + /** + * Update bulkEditSelectedFields for the share field from dt-users-connection value. + * Called on initial value and on change; lives at module scope to avoid redefining per render. + */ + function processShareComponentValue(fieldKey, componentValue) { + const currentFieldData = bulkEditSelectedFields.find( + (f) => f.fieldKey === fieldKey, + ); + if (!currentFieldData) { + return; + } + + const items = normalizeShareComponentItems(componentValue); + + if (items.length > 0) { + const userIds = items + .map((item) => { + const userId = item.id || item.user_id || null; + return userId ? parseInt(userId, 10) : null; + }) + .filter((id) => id !== null); + + currentFieldData.shareUserIds = userIds; + currentFieldData.shareUserLabels = {}; + items.forEach((item) => { + const userId = item.id || item.user_id; + if (userId) { + currentFieldData.shareUserLabels[userId] = item.label || ''; + } + }); + currentFieldData.shareUserId = userIds.length > 0 ? userIds[0] : null; + currentFieldData.shareUserLabel = + userIds.length > 0 + ? currentFieldData.shareUserLabels[userIds[0]] || '' + : null; + } else { + currentFieldData.shareUserIds = []; + currentFieldData.shareUserLabels = {}; + currentFieldData.shareUserId = null; + currentFieldData.shareUserLabel = null; + } + } + function collectFieldValue(fieldKey, fieldType, fieldWrapper) { // Special case: comment field (not a real post field type) if (fieldType === 'comment') { @@ -1974,63 +2022,17 @@ try { window.componentService.initialize(); } catch (e) { - // ComponentService initialization error - component should still work - } - } - - // Function to process component value and update fieldData - // dt-users-connection provides user IDs directly - function processShareComponentValue(componentValue) { - const currentFieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - if (!currentFieldData) { - return; - } - - const items = normalizeShareComponentItems(componentValue); - - if (items.length > 0) { - // dt-users-connection format: [{id: , type: 'user', label: }] - // Extract user IDs directly - no conversion needed! - const userIds = items - .map((item) => { - // item.id is the user ID directly - const userId = item.id || item.user_id || null; - return userId ? parseInt(userId, 10) : null; - }) - .filter((id) => id !== null); - - // Store user IDs and labels - currentFieldData.shareUserIds = userIds; - currentFieldData.shareUserLabels = {}; - items.forEach((item) => { - const userId = item.id || item.user_id; - if (userId) { - currentFieldData.shareUserLabels[userId] = item.label || ''; - } - }); - - // For backward compatibility, also store first user ID - currentFieldData.shareUserId = - userIds.length > 0 ? userIds[0] : null; - currentFieldData.shareUserLabel = - userIds.length > 0 - ? currentFieldData.shareUserLabels[userIds[0]] || '' - : null; - } else { - // Value is empty array or invalid format - currentFieldData.shareUserIds = []; - currentFieldData.shareUserLabels = {}; - currentFieldData.shareUserId = null; - currentFieldData.shareUserLabel = null; + console.error( + 'ComponentService initialization error (share field):', + e, + ); } } - // Process initial value if present + // Process initial value (initialValue is already set via HTML value attribute) if (shareComponent.value) { try { - processShareComponentValue(shareComponent.value); + processShareComponentValue(fieldKey, shareComponent.value); } catch (err) { console.error('Error processing initial share value:', err); } @@ -2039,42 +2041,11 @@ // Listen for value changes to store user IDs (supports multiple selections) shareComponent.addEventListener('change', function () { try { - processShareComponentValue(this.value); + processShareComponentValue(fieldKey, this.value); } catch (err) { console.error('Error processing share change value:', err); } }); - - // If we have a previous user ID, restore it in the component - if (previousUserId) { - const label = - fieldData?.shareUserLabels?.[previousUserId] || - fieldData?.shareUserLabel || - ''; - - if (label && shareComponent) { - const value = JSON.stringify([ - { - id: parseInt(previousUserId, 10), - type: 'user', - label, - }, - ]); - shareComponent.value = value; - // Update fieldData - const currentFieldData = bulkEditSelectedFields.find( - (f) => f.fieldKey === fieldKey, - ); - if (currentFieldData) { - currentFieldData.shareUserId = previousUserId; - currentFieldData.shareUserIds = [parseInt(previousUserId, 10)]; - currentFieldData.shareUserLabel = label; - currentFieldData.shareUserLabels = { - [previousUserId]: label, - }; - } - } - } }); return; @@ -2147,7 +2118,7 @@ try { window.componentService.initialize(); } catch (e) { - // ComponentService initialization error - components should still work + console.error('ComponentService initialization error:', e); } } @@ -2509,7 +2480,7 @@ try { window.componentService.initialize(); } catch (e) { - // ComponentService initialization error + console.error('ComponentService initialization error:', e); } } initializeBulkEditFieldHandlers(fieldKey, fieldType); From ce26e590ac37b8a42aab05cccb6f67651248e2cb Mon Sep 17 00:00:00 2001 From: corsac Date: Mon, 2 Mar 2026 11:57:43 +0100 Subject: [PATCH 10/10] remove artifact comments --- dt-assets/js/modular-list.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/dt-assets/js/modular-list.js b/dt-assets/js/modular-list.js index 0b12d721f9..bef66e06ac 100644 --- a/dt-assets/js/modular-list.js +++ b/dt-assets/js/modular-list.js @@ -1097,12 +1097,6 @@ window.location.reload(); }); - // ============================================ - // Bulk Edit Field Selection with dt-multi-select - // NOTE: This functionality has been moved to modular-list-bulk.js - // The bulk module registers itself via DT_List.bulk and handles all bulk edit field selection - // ============================================ - archivedSwitch.on('click', function () { const showArchived = this.checked; @@ -3068,13 +3062,6 @@ }); } - /*** - * Bulk Edit - * NOTE: Bulk edit functionality has been moved to modular-list-bulk.js - * The bulk module handles all bulk edit operations including checkbox events, - * field selection, and submit handling - */ - /** * Split By Feature */