Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 87 additions & 138 deletions dt-assets/js/modular-list-bulk.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

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

@kodinkat this looks like a whole lot of code handling maybes.
Can we double check the actual values being passed arround?

Copy link
Member

Choose a reason for hiding this comment

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

@kodinkat, my question still stands. Why handle every single possible value, instead of pulling from what the component actually returns?

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.
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 `<div class="assigned-to-row" dir="auto">
<span>
<span class="avatar"><img style="vertical-align: text-bottom" src="{{avatar}}"/></span>
${window.SHAREDFUNCTIONS.escapeHTML(item.name)}
</span>
${item.status_color ? `<span class="status-square" style="background-color: ${window.SHAREDFUNCTIONS.escapeHTML(item.status_color)};">&nbsp;</span>` : ''}
${
item.update_needed && item.update_needed > 0
? `<span>
<img style="height: 12px;" src="${window.SHAREDFUNCTIONS.escapeHTML(window.wpApiShare.template_dir)}/dt-assets/images/broken.svg"/>
<span style="font-size: 14px">${window.SHAREDFUNCTIONS.escapeHTML(item.update_needed)}</span>
</span>`
: ''
}
</div>`;
},
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(
`<span class="selected-result">${window.SHAREDFUNCTIONS.escapeHTML(item.name)}</span>`,
);

// 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
*/
Expand Down Expand Up @@ -2361,20 +2281,33 @@
displayHtml += '<em>No option selected</em>';
}
} 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 += `<span class="label" style="opacity: 0.6; margin-right: 5px; margin-bottom: 5px; display: inline-block;">${window.SHAREDFUNCTIONS.escapeHTML(userName)}</span>`;
} else {
displayHtml += '<em>No user selected</em>';
}
displayHtml += `<span class="label" style="opacity: 0.6; margin-right: 5px; margin-bottom: 5px; display: inline-block;">${window.SHAREDFUNCTIONS.escapeHTML(userName)}</span>`;
} else {
displayHtml += '<em>No user selected</em>';
}
Expand Down Expand Up @@ -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 &&
Expand All @@ -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(
Expand Down Expand Up @@ -2483,7 +2432,7 @@
console.error('ComponentService initialization error:', e);
}
}
initializeBulkEditFieldHandlers(fieldKey, fieldType);

Check failure on line 2435 in dt-assets/js/modular-list-bulk.js

View workflow job for this annotation

GitHub Actions / PHP 7.4

'initializeBulkEditFieldHandlers' is not defined

Check failure on line 2435 in dt-assets/js/modular-list-bulk.js

View workflow job for this annotation

GitHub Actions / PHP 8.4

'initializeBulkEditFieldHandlers' is not defined
});
});
}
Expand Down
24 changes: 1 addition & 23 deletions dt-assets/js/shared-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1040,29 +1040,7 @@ window.SHAREDFUNCTIONS = {
}

case 'user_select': {
// user_select uses legacy typeahead (not a web component)
const fieldId = id;
return `<div id="${fieldKey}" class="${fieldId} dt_user_select">
<var id="${fieldId}-result-container" class="result-container ${fieldId}-result-container"></var>
<div id="${fieldId}_t" name="form-${fieldId}" class="scrollable-typeahead">
<div class="typeahead__container" style="margin-bottom: 0">
<div class="typeahead__field">
<span class="typeahead__query">
<input class="js-typeahead-${fieldId} input-height" dir="auto"
name="${fieldId}[query]" placeholder="${window.SHAREDFUNCTIONS.escapeHTML(window.wpApiShare?.translations?.search_users || 'Search Users')}"
data-field_type="user_select"
data-field="${fieldKey}"
autocomplete="off">
</span>
<span class="typeahead__button">
<button type="button" class="search_${fieldKey} typeahead__image_button input-height" data-id="${fieldKey}">
<img src="${window.SHAREDFUNCTIONS.escapeHTML(window.wpApiShare.template_dir)}/dt-assets/images/chevron_down.svg"/>
</button>
</span>
</div>
</div>
</div>
</div>`;
return `<dt-users-connection ${baseAttrs} single></dt-users-connection>`;
}

case 'communication_channel':
Expand Down
1 change: 1 addition & 0 deletions dt-assets/scss/_list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading