diff --git a/dt-assets/js/modular-list-bulk.js b/dt-assets/js/modular-list-bulk.js index 7b055e334..4cac3fc6a 100644 --- a/dt-assets/js/modular-list-bulk.js +++ b/dt-assets/js/modular-list-bulk.js @@ -822,6 +822,11 @@ // For boolean fields, include even if false (false is a valid value) if (fieldType === 'boolean') { updatePayload[fieldKey] = fieldValue === true; + } else if (fieldType === 'user_select') { + const normalizedUser = normalizeUserSelectValue(fieldValue); + if (normalizedUser !== null) { + updatePayload[fieldKey] = normalizedUser; + } } else { updatePayload[fieldKey] = fieldValue; } @@ -918,6 +923,45 @@ return Array.isArray(value) ? value : []; } + /** + * Normalize a user_select value to the "user-{id}" string format expected by + * the backend and conditional-removal logic. + * + * In practice we only see: + * - numbers from ComponentService.convertValue('DT-USERS-CONNECTION', ...) + * - strings like "user-5" or "5" from API/legacy data + */ + function normalizeUserSelectValue(rawValue) { + if (rawValue === null || rawValue === undefined) { + return null; + } + + // Numbers (ComponentService.convertValue for dt-users-connection in single + // mode returns a plain integer user ID, or 0/"" when empty) + if (typeof rawValue === 'number') { + return rawValue > 0 ? `user-${rawValue}` : null; + } + + // Strings + if (typeof rawValue === 'string') { + const trimmed = rawValue.trim(); + if (!trimmed) { + return null; + } + if (trimmed.startsWith('user-')) { + return trimmed; + } + if (/^\d+$/.test(trimmed)) { + return `user-${trimmed}`; + } + // Any other free-form string (e.g. display name) is not a valid payload + // for the backend user_select handler. + return null; + } + + return null; + } + /** * 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. @@ -1100,32 +1144,6 @@ 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 = @@ -2121,107 +2139,9 @@ console.error('ComponentService initialization error:', e); } } - - // 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) { - // Destroy existing typeahead instance if it exists (for restore scenarios) - const typeaheadSelector = `.js-typeahead-${fieldId}`; - if (window.Typeahead && window.Typeahead[typeaheadSelector]) { - try { - // Try to destroy the existing instance - if (window.Typeahead[typeaheadSelector].destroy) { - window.Typeahead[typeaheadSelector].destroy(); - } - delete window.Typeahead[typeaheadSelector]; - } catch (e) { - // If destroy fails, just delete the reference - delete window.Typeahead[typeaheadSelector]; - } - } - - // Initialize typeahead - $.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 - } - /** * Field Clear/Restore Handlers */ @@ -2361,20 +2281,33 @@ displayHtml += 'No option selected'; } } else if (fieldType === 'user_select') { - // user_select returns "user-{id}" format + // user_select returns "user-{id}" for payloads, but the dt-users-connection + // component exposes richer objects for display. Support both shapes. if (valuesToRemove) { - const userId = valuesToRemove.replace('user-', ''); - // Try to get user name from typeahead data or make a simple display - const fieldId = `bulk_${fieldKey}`; - const userInput = $(`.js-typeahead-${fieldId}`); - let userName = `User ${userId}`; - if (userInput.length > 0) { - const storedName = userInput.data('selected-user-name'); - if (storedName) { - userName = storedName; + let userId = null; + let userName = null; + + if (Array.isArray(valuesToRemove)) { + const first = valuesToRemove[0]; + if (first) { + userId = first.id || first.user_id || null; + userName = first.label || first.name || null; } + } else if (typeof valuesToRemove === 'object') { + userId = valuesToRemove.id || valuesToRemove.user_id || null; + userName = valuesToRemove.label || valuesToRemove.name || null; + } else if (typeof valuesToRemove === 'string') { + userId = valuesToRemove.replace('user-', ''); + } + + if (userId) { + if (!userName) { + userName = `User ${userId}`; + } + displayHtml += `${window.SHAREDFUNCTIONS.escapeHTML(userName)}`; + } else { + displayHtml += 'No user selected'; } - displayHtml += `${window.SHAREDFUNCTIONS.escapeHTML(userName)}`; } else { displayHtml += 'No user selected'; } @@ -2420,9 +2353,18 @@ if (component && component.value) { rawValueWithLabels = component.value; } + } else if (fieldType === 'user_select') { + const usersComponent = fieldWrapper.find('dt-users-connection')[0]; + if (usersComponent && usersComponent.value) { + const items = normalizeShareComponentItems(usersComponent.value); + if (items.length > 0) { + rawValueWithLabels = items; + } + } } let hasValues = false; + let normalizedUserValueToRemove = null; if ( currentValue !== null && currentValue !== undefined && @@ -2435,18 +2377,25 @@ ) { const values = currentValue?.values || currentValue; hasValues = Array.isArray(values) && values.length > 0; - } else if (fieldType === 'key_select' || fieldType === 'user_select') { + } else if (fieldType === 'key_select') { hasValues = true; + } else if (fieldType === 'user_select') { + normalizedUserValueToRemove = normalizeUserSelectValue(currentValue); + hasValues = !!normalizedUserValueToRemove; } } if (supportsSelectiveRemoval && hasValues) { fieldData.operation = 'remove'; - fieldData.valuesToRemove = currentValue; + fieldData.valuesToRemove = + fieldType === 'user_select' + ? normalizedUserValueToRemove + : currentValue; fieldData.rawValueWithLabels = rawValueWithLabels; const displayValue = - fieldType === 'connection' && rawValueWithLabels + (fieldType === 'connection' || fieldType === 'user_select') && + rawValueWithLabels ? rawValueWithLabels : currentValue; const displayHtml = renderValuesToRemoveDisplay( diff --git a/dt-assets/js/shared-functions.js b/dt-assets/js/shared-functions.js index 53617ed07..e760a4fc3 100644 --- a/dt-assets/js/shared-functions.js +++ b/dt-assets/js/shared-functions.js @@ -1040,29 +1040,7 @@ window.SHAREDFUNCTIONS = { } case 'user_select': { - // user_select uses legacy typeahead (not a web component) - const fieldId = id; - return `
- -
-
-
- - - - - - -
-
-
-
`; + return ``; } case 'communication_channel': diff --git a/dt-assets/scss/_list.scss b/dt-assets/scss/_list.scss index ba1d59a96..89f4c40d3 100644 --- a/dt-assets/scss/_list.scss +++ b/dt-assets/scss/_list.scss @@ -691,6 +691,7 @@ table.js-list { dt-datetime label, dt-number label, dt-tags label, + dt-users-connection label, dt-user-select label, dt-connection label, dt-location label {