diff --git a/dt-groups/base-setup.php b/dt-groups/base-setup.php index ab16189b7..d70a528cc 100644 --- a/dt-groups/base-setup.php +++ b/dt-groups/base-setup.php @@ -1196,6 +1196,48 @@ public function scripts(){ } } + // Filter fields that should be shown in create form + // Note: title field is handled separately in JavaScript, so we exclude it here + $create_form_fields = []; + foreach ( $field_settings as $field_key => $field_setting ) { + // Skip title field - it's rendered separately in JavaScript + if ( $field_key === 'title' ) { + continue; + } + + // Skip user_select (e.g. assigned_to) - not part of Add Child flow; uses legacy typeahead, not collected + if ( isset( $field_setting['type'] ) && $field_setting['type'] === 'user_select' ) { + continue; + } + + // Skip hidden fields unless they have custom_display + if ( !empty( $field_setting['hidden'] ) && empty( $field_setting['custom_display'] ) ) { + continue; + } + // Skip fields explicitly set to not show in create form + if ( isset( $field_setting['in_create_form'] ) && $field_setting['in_create_form'] === false ) { + continue; + } + // Include fields that have in_create_form => true or list this post type in the array + if ( !empty( $field_setting['in_create_form'] ) ) { + $in_form = $field_setting['in_create_form']; + if ( $in_form === true ) { + $create_form_fields[ $field_key ] = $field_setting; + } elseif ( is_array( $in_form ) && in_array( $this->post_type, $in_form, true ) ) { + $create_form_fields[ $field_key ] = $field_setting; + } + } + } + + // Title field is rendered separately in JS; pass its settings as a dedicated key (not in fieldSettings loop). + $title_field_setting = isset( $field_settings['title'] ) ? $field_settings['title'] : null; + + // Get mapbox token for location fields + $mapbox_key = ''; + if ( class_exists( 'DT_Mapbox_API' ) ) { + $mapbox_key = DT_Mapbox_API::get_key() ?? ''; + } + wp_localize_script( $genmap_script_handle, 'dtGroupGenmap', [ 'statusField' => [ 'key' => $status_key, @@ -1204,18 +1246,32 @@ public function scripts(){ ], 'groupTypes' => $group_type_labels, 'groupTypeIcons' => $group_type_icons, + 'fieldSettings' => $create_form_fields, + 'titleFieldSetting' => $title_field_setting, + 'mapboxKey' => $mapbox_key, 'strings' => [ 'loading' => __( 'Loading map…', 'disciple_tools' ), 'error' => __( 'Unable to load generational map.', 'disciple_tools' ), 'empty' => __( 'No child groups to display.', 'disciple_tools' ), + 'chart_aria' => __( 'Group generational map', 'disciple_tools' ), 'details' => [ 'open' => __( 'Open', 'disciple_tools' ), 'add' => __( 'Add', 'disciple_tools' ), + 'close' => __( 'Close', 'disciple_tools' ), + 'type' => __( 'Type', 'disciple_tools' ), + 'status' => __( 'Status', 'disciple_tools' ), + 'members' => __( 'Members', 'disciple_tools' ), + 'assigned' => __( 'Assigned', 'disciple_tools' ), + 'expand' => __( 'Expand', 'disciple_tools' ), + 'collapse' => __( 'Collapse', 'disciple_tools' ), ], 'modal' => [ 'add_child_title' => __( 'Add Child To', 'disciple_tools' ), 'add_child_name_title' => __( 'Name', 'disciple_tools' ), 'add_child_but' => __( 'Add Child', 'disciple_tools' ), + 'name_required' => __( 'Name is required', 'disciple_tools' ), + 'error_creating_child' => __( 'Error creating child group: %s', 'disciple_tools' ), + 'unknown_error' => __( 'Unknown error', 'disciple_tools' ), ], ], 'recordUrlBase' => trailingslashit( site_url() ), diff --git a/dt-groups/genmap-d3.css b/dt-groups/genmap-d3.css index da5d10ad6..2f00d7469 100644 --- a/dt-groups/genmap-d3.css +++ b/dt-groups/genmap-d3.css @@ -396,3 +396,12 @@ outline: 2px solid #eed936; outline-offset: 2px; } + +/* Add Child modal (Genmapper): allow location dropdown to extend outside (avoid clipping) */ +#template_metrics_modal.genmap-add-child-modal { + overflow: visible; +} + +#template_metrics_modal.genmap-add-child-modal #template_metrics_modal_content .form-field-location { + overflow: visible; +} diff --git a/dt-groups/genmap-tile.js b/dt-groups/genmap-tile.js index 7d2e3359e..8f3e2f884 100644 --- a/dt-groups/genmap-tile.js +++ b/dt-groups/genmap-tile.js @@ -6,6 +6,7 @@ const MAX_CANVAS_HEIGHT = 320; const MIN_CANVAS_HEIGHT = 220; const LAYOUT_STORAGE_KEY = 'group_genmap_layout'; + const GENMAP_ADD_CHILD_FIELD_PREFIX = 'group_genmap_add_child_'; // Node dimensions constants const NODE_WIDTH = 72; // Increased from 60px to prevent text clipping @@ -658,6 +659,7 @@ * @returns {string} HTML content */ function buildPopoverContent(nodeData) { + // Generation number is intentionally not shown in the popover (see issue #2878). const data = nodeData.data; const strings = window.dtGroupGenmap?.strings || {}; const detailsStrings = strings.details || {}; @@ -677,13 +679,13 @@ // Build HTML with header containing title and close button let html = '
'; html += `

${window.lodash.escape(data.name || '')}

`; - html += ''; + html += ``; html += '
'; // Group type with icon if (groupTypeLabel) { html += '
'; - html += 'Type:'; + html += `${window.lodash.escape(detailsStrings.type || 'Type')}:`; if (groupTypeIcon) { html += `${window.lodash.escape(groupTypeLabel)}`; } else { @@ -696,19 +698,11 @@ if (status) { const statusColor = data.statusColor || '#3f729b'; html += '
'; - html += 'Status:'; + html += `${window.lodash.escape(detailsStrings.status || 'Status')}:`; html += `${window.lodash.escape(status)}`; html += '
'; } - // Generation - if (data.content) { - html += '
'; - html += 'Generation:'; - html += `${window.lodash.escape(data.content)}`; - html += '
'; - } - // Actions html += '
'; @@ -725,8 +719,10 @@ (nodeData._children && nodeData._children.length > 0) ) { const isCollapsed = nodeData.data.collapsed || false; - const collapseText = isCollapsed ? 'Expand' : 'Collapse'; - html += ``; + const collapseText = isCollapsed + ? detailsStrings.expand || 'Expand' + : detailsStrings.collapse || 'Collapse'; + html += ``; } html += '
'; @@ -1993,7 +1989,7 @@ style="width: 100%; height: 100%;">
`; @@ -2103,27 +2099,36 @@ let detailsHtml = '
'; detailsHtml += '
'; + const detailsLabels = window.dtGroupGenmap?.strings?.details || {}; if (data.group_status && data.group_status.label) { detailsHtml += - '

Status: ' + + '

' + + window.lodash.escape(detailsLabels.status || 'Status') + + ': ' + window.lodash.escape(data.group_status.label) + '

'; } if (data.group_type && data.group_type.label) { detailsHtml += - '

Type: ' + + '

' + + window.lodash.escape(detailsLabels.type || 'Type') + + ': ' + window.lodash.escape(data.group_type.label) + '

'; } if (data.member_count !== undefined) { detailsHtml += - '

Members: ' + + '

' + + window.lodash.escape(detailsLabels.members || 'Members') + + ': ' + window.lodash.escape(data.member_count) + '

'; } if (data.assigned_to && data.assigned_to.display) { detailsHtml += - '

Assigned: ' + + '

' + + window.lodash.escape(detailsLabels.assigned || 'Assigned') + + ': ' + window.lodash.escape(data.assigned_to.display) + '

'; } @@ -2177,19 +2182,95 @@ } const modalStrings = window.dtGroupGenmap?.strings?.modal || {}; - const listHtml = ` - - - `; + )}" />`; + + // fieldSettings contains only in_create_form fields (title excluded in PHP). Title is rendered from titleFieldSetting. + const fieldsToRender = {}; + Object.keys(fieldSettings).forEach((key) => { + // Skip title (handled separately) and legacy "name" field if present to avoid duplicating the label. + if (key !== 'title' && key !== 'name') { + fieldsToRender[key] = fieldSettings[key]; + } + }); + + // Title field is passed as a separate top-level key from PHP (titleFieldSetting) + const titleFieldSetting = window.dtGroupGenmap?.titleFieldSetting || { + name: modalStrings.add_child_name_title || 'Name', + type: 'text', + }; + + if (window.SHAREDFUNCTIONS && window.SHAREDFUNCTIONS.renderField) { + const titleFieldHtml = window.SHAREDFUNCTIONS.renderField( + 'title', + titleFieldSetting, + fieldPrefix, + ); + if (titleFieldHtml) { + listHtml += `
${titleFieldHtml}
`; + } else { + // Fallback if renderField returns null + listHtml += ` + `; + } + } else { + // Fallback if web components not available + listHtml += ` + `; + } + + // Render other fields that have in_create_form => true + const hasRenderField = !!( + window.SHAREDFUNCTIONS && window.SHAREDFUNCTIONS.renderField + ); + if (Object.keys(fieldsToRender).length > 0 && !hasRenderField) { + console.warn( + 'Genmapper Add Child: SHAREDFUNCTIONS.renderField is unavailable; in_create_form fields other than title will not be rendered.', + ); + } + Object.keys(fieldsToRender).forEach((fieldKey) => { + // Safety guard in case title/name ever slip into fieldsToRender. + if (fieldKey === 'title' || fieldKey === 'name') { + return; + } + + const fieldSetting = fieldsToRender[fieldKey]; + // Only render fields that are supported by renderField + if (hasRenderField && fieldSetting && fieldSetting.type) { + const fieldHtml = window.SHAREDFUNCTIONS.renderField( + fieldKey, + fieldSetting, + fieldPrefix, + ); - const buttonsHtml = ``; @@ -2205,46 +2286,203 @@ return; } + // Mark this usage so CSS can safely relax overflow rules without affecting other metrics modals + modal.addClass('genmap-add-child-modal'); + modal.css('overflow', 'visible'); + jQuery(modalButtons).empty().html(buttonsHtml); jQuery('#template_metrics_modal_title') .empty() .html(window.lodash.escape(title)); - jQuery(content).css('max-height', '300px'); - jQuery(content).css('overflow', 'auto'); + jQuery(content).css('max-height', '400px'); + jQuery(content).css('overflow-y', 'visible'); jQuery(content).empty().html(listHtml); jQuery(modal).foundation('open'); + + // Set mapbox token on location components after insertion (avoids fragile HTML string replace) + const mapboxKey = window.dtGroupGenmap?.mapboxKey || ''; + if (mapboxKey) { + content.find('dt-location-map').each(function () { + this.setAttribute('mapbox-token', mapboxKey); + }); + } + // Location dropdown visibility is handled by #template_metrics_modal_content .form-field-location { overflow: visible } in genmap-d3.css } function handleAddChild() { - const postType = jQuery('#group_genmap_add_child_post_type').val(); - const parentId = jQuery('#group_genmap_add_child_post_id').val(); - const childTitle = jQuery('#group_genmap_add_child_name').val(); + const fieldPrefix = GENMAP_ADD_CHILD_FIELD_PREFIX; + const postType = jQuery(`#${fieldPrefix}post_type`).val(); + const parentId = parseInt(jQuery(`#${fieldPrefix}post_id`).val(), 10); + const modalContent = jQuery('#template_metrics_modal_content'); - if (!postType || !parentId || !childTitle) { + if (!postType || !parentId) { return; } + // Collect values from web components + const fields = {}; + const fieldSettings = window.dtGroupGenmap?.fieldSettings || {}; + + // Get title field value (required) + const titleField = modalContent.find(`#${fieldPrefix}title`)[0]; + if (titleField) { + if (titleField.tagName && titleField.tagName.startsWith('DT-')) { + // Web component - get value directly + if (titleField.value) { + fields.title = titleField.value; + } + } else { + // Fallback for regular input + const titleValue = jQuery(titleField).val(); + if (titleValue) { + fields.title = titleValue; + } + } + } + + // Validate title is present + if ( + !fields.title || + (typeof fields.title === 'string' && fields.title.trim() === '') + ) { + const msg = + window.dtGroupGenmap?.strings?.modal?.name_required || + 'Name is required'; + alert(msg); + return; + } + + const submitBtn = jQuery(`#${fieldPrefix}but`); + submitBtn.prop('disabled', true).addClass('loading'); + + // Collect values from all other web components + modalContent + .find( + 'dt-text, dt-textarea, dt-number, dt-toggle, dt-date, dt-single-select, dt-multi-select-button-group, dt-tags, dt-connection, dt-location-map, dt-multi-text', + ) + .each(function () { + const component = this; + const rawId = component.id || ''; + const idWithoutPrefix = rawId.startsWith(fieldPrefix) + ? rawId.slice(fieldPrefix.length) + : rawId; + const fieldKey = component.name || idWithoutPrefix; + + // Skip hidden fields and system fields + if ( + !fieldKey || + fieldKey === 'post_type' || + fieldKey === 'post_id' || + fieldKey === 'title' || + fieldKey === 'name' + ) { + return; + } + + const fieldSetting = fieldSettings[fieldKey]; + if (!fieldSetting) { + return; + } + + // Get value from component + if ( + component.value === undefined || + component.value === null || + component.value === '' + ) { + return; + } + + let value = component.value; + + // Convert value using ComponentService if available + if (window.DtWebComponents && window.DtWebComponents.ComponentService) { + try { + value = window.DtWebComponents.ComponentService.convertValue( + component.tagName, + value, + ); + } catch (e) { + console.warn('Error converting value for field', fieldKey, e); + // Continue with original value if conversion fails + } + } + + // Format value based on field type for API + const fieldType = fieldSetting.type; + + switch (fieldType) { + case 'key_select': + // key_select: ComponentService returns the key, use directly + fields[fieldKey] = value; + break; + case 'multi_select': + // multi_select: ComponentService returns array, format for API + if (Array.isArray(value) && value.length > 0) { + fields[fieldKey] = { values: value.map((v) => ({ value: v })) }; + } + break; + case 'connection': + // connection: ComponentService returns array of IDs, format for API + if (Array.isArray(value) && value.length > 0) { + fields[fieldKey] = { values: value.map((v) => ({ value: v })) }; + } else if (value) { + fields[fieldKey] = { values: [{ value: value }] }; + } + break; + case 'communication_channel': + // communication_channel: ComponentService returns array of objects + if (Array.isArray(value) && value.length > 0) { + fields[fieldKey] = value; + } + break; + case 'tags': + // tags: ComponentService returns array, format for API + if (Array.isArray(value) && value.length > 0) { + fields[fieldKey] = { values: value.map((v) => ({ value: v })) }; + } else if (value) { + fields[fieldKey] = { values: [{ value: value }] }; + } + break; + default: + // For text, textarea, number, boolean, date, etc., use value directly + fields[fieldKey] = value; + break; + } + }); + + // Parent connection is set via additional_meta (API overwrites connection from it) + const createPayload = { + ...fields, + additional_meta: { + created_from: parentId, + add_connection: 'child_groups', + }, + }; + if (window.API && window.API.create_post) { - window.API.create_post(postType, { - title: childTitle, - additional_meta: { - created_from: parentId, - add_connection: 'child_groups', - }, - }) + window.API.create_post(postType, createPayload) .then((newPost) => { + submitBtn.prop('disabled', false).removeClass('loading'); jQuery('#template_metrics_modal').foundation('close'); // Refresh the page to show the new child window.location.reload(); }) .catch(function (error) { + submitBtn.prop('disabled', false).removeClass('loading'); console.error(error); - alert( - 'Error creating child group: ' + (error.message || 'Unknown error'), - ); + const template = + window.dtGroupGenmap?.strings?.modal?.error_creating_child || + 'Error creating child group: %s'; + const unknownErr = + window.dtGroupGenmap?.strings?.modal?.unknown_error || + 'Unknown error'; + const msg = template.replace('%s', error.message || unknownErr); + alert(msg); }); } else { + submitBtn.prop('disabled', false).removeClass('loading'); console.error('window.API.create_post is not available'); } } @@ -2281,11 +2519,23 @@ handleAddChild(); }); + // When the shared metrics modal closes, clear any Genmapper-specific modal classes + jQuery(document).on( + 'closed.zf.reveal', + '#template_metrics_modal[data-reveal]', + function () { + jQuery('#template_metrics_modal').removeClass('genmap-add-child-modal'); + }, + ); + jQuery(document).on( 'open.zf.reveal', '#template_metrics_modal[data-reveal]', function () { - jQuery('#group_genmap_add_child_name').focus(); + const titleField = jQuery('#group_genmap_add_child_title'); + if (titleField.length) { + titleField[0].focus(); + } }, );